From 5d304a69655916602c59aba549f264d9dddf8ff1 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Mon, 6 Jul 2026 21:53:43 -0400 Subject: [PATCH 01/19] feat(capi): callback Canvas so C embedders can render primitives themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tile57_chart_render_view_cb — render_view's GPU/vector twin. It runs the SAME view portrayal (resolve → flatten → declutter → paint-order) but forwards every resolved primitive through a C function-pointer table (tile57_canvas_cb) instead of rasterising to PNG. A C library that draws with its own renderer (GPU or vector) feeds these straight into it. - cb_canvas.zig: a third cv.Canvas implementation (alongside RasterCanvas and PdfCanvas) that flattens each ring set into a flat C vertex array + ring offsets and calls fill_path / stroke_path / fill_pattern / draw_glyphs. - pixel.zig: new Output.callback branch emits the NODTA base fill then replays paintOps through the callback canvas; returns an empty buffer. - renderView gains an optional cb_table arg; PNG/PDF callers pass null. - include/tile57.h: the tile57_point/tile57_rgba/tile57_rings/tile57_canvas_cb ABI and tile57_chart_render_view_cb, same INVERTED return convention as render_view (0 ok / -1 bad handle / -2 render failure / -3 unsupported). Geometry is emitted in canvas pixel space (y down), in final paint order, with colours resolved for the active palette. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 39 +++++++++++++ src/capi.zig | 39 ++++++++++++- src/chart.zig | 3 +- src/render/cb_canvas.zig | 119 +++++++++++++++++++++++++++++++++++++++ src/render/pixel.zig | 24 +++++++- src/render/render.zig | 1 + tools/ascii.zig | 4 +- 7 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 src/render/cb_canvas.zig diff --git a/include/tile57.h b/include/tile57.h index f3e594e..f6fbfa0 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -256,6 +256,45 @@ int tile57_chart_render_pdf(tile57_chart *chart, double lon, double lat, double const struct tile57_mariner *m, uint8_t **out, size_t *out_len); +/* ---- callback Canvas: tile57_chart_render_view's GPU/vector twin ---------- + * Run the SAME view portrayal as tile57_chart_render_view, but paint every + * resolved, flattened primitive through a table of C function pointers instead + * of rasterising to PNG. The embedder (e.g. a GPU chart plugin) feeds these to + * its own renderer. Geometry is emitted in canvas PIXEL space (y down), in + * final paint order; colours are fully resolved for the active palette. */ +typedef struct { float x, y; } tile57_point; /* canvas pixels */ +typedef struct { uint8_t r, g, b, a; } tile57_rgba; /* resolved straight-alpha */ +/* A multi-ring path: flat vertex array `pts`; ring k spans + * [ring_starts[k], ring_starts[k+1]) (last runs to `n`). Rings closed implicitly. */ +typedef struct { + const tile57_point *pts; uint32_t n; + const uint32_t *ring_starts; uint32_t ring_count; +} tile57_rings; +/* The paint table. Every callback gets `ctx` back verbatim. Calls arrive in + * paint order (no priority key needed). */ +typedef struct { + void *ctx; + /* Fill closed rings; even_odd != 0 selects the even-odd rule. */ + void (*fill_path) (void *ctx, const tile57_rings *rings, tile57_rgba color, int even_odd); + /* Stroke polylines width_px wide; dash on/off in px (0,0 = solid). */ + void (*stroke_path) (void *ctx, const tile57_rings *rings, float width_px, + float dash_on, float dash_off, tile57_rgba color); + /* Fill rings with a repeating RGBA8 pattern cell (pw*ph*4 bytes). */ + void (*fill_pattern)(void *ctx, const tile57_rings *rings, uint32_t pw, uint32_t ph, + const uint8_t *rgba); + /* Draw a shaped label as flattened outline rings (px), optional halo + * (halo.a == 0 => none). */ + void (*draw_glyphs) (void *ctx, const tile57_rings *outline, tile57_rgba color, + tile57_rgba halo, float halo_px); +} tile57_canvas_cb; +/* Returns (INVERTED, matches tile57_chart_render_view): 0 ok / -1 bad handle / + * -2 render failure / -3 unsupported source. Same threading rules as the rest + * of a tile57_chart (serialise per handle). */ +int tile57_chart_render_view_cb(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const struct tile57_mariner *m, + const tile57_canvas_cb *canvas); + /* The chart's per-cell metadata as a JSON array, one object per cell: * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", * "issueDate":"20240105","agency":550,"bbox":[west,south,east,north]}, ...] diff --git a/src/capi.zig b/src/capi.zig index 76865b8..795e95c 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -327,7 +327,7 @@ export fn tile57_chart_render_view( .dusk => .dusk, .night => .night, }; - const bytes = c.renderView(lon, lat, zoom, width, height, palette, &settings, .png) catch |e| switch (e) { + const bytes = c.renderView(lon, lat, zoom, width, height, palette, &settings, .png, null) catch |e| switch (e) { error.Unsupported => return -3, else => return -2, }; @@ -431,7 +431,7 @@ export fn tile57_chart_render_pdf( .dusk => .dusk, .night => .night, }; - const bytes = c.renderView(lon, lat, zoom, width, height, palette, &settings, .pdf) catch |e| switch (e) { + const bytes = c.renderView(lon, lat, zoom, width, height, palette, &settings, .pdf, null) catch |e| switch (e) { error.Unsupported => return -3, else => return -2, }; @@ -440,6 +440,41 @@ export fn tile57_chart_render_pdf( return 0; } +const CbCanvas = @import("render").cb_canvas.CCanvas; + +/// tile57_chart_render_view's GPU/vector twin: run the SAME view portrayal, but +/// paint every resolved, flattened primitive through the C callback table +/// `canvas` (see tile57.h) instead of rasterising. Geometry is emitted in canvas +/// PIXEL space (y down) in paint order; colours are resolved for the palette. +/// Same INVERTED return convention as tile57_chart_render_view: +/// 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. +export fn tile57_chart_render_view_cb( + handle: ?*Chart, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + canvas: ?*const CbCanvas, +) callconv(.c) c_int { + const c = handle orelse return -1; + const cb = canvas orelse return -2; + if (width == 0 or height == 0 or width > 16384 or height > 16384) return -2; + const settings: chartstyle.MarinerSettings = if (m) |p| marinerFromC(p) else .{}; + const palette: RenderPalette = switch (settings.scheme) { + .day => .day, + .dusk => .dusk, + .night => .night, + }; + const bytes = c.renderView(lon, lat, zoom, width, height, palette, &settings, .callback, cb) catch |e| switch (e) { + error.Unsupported => return -3, + else => return -2, + }; + chart.freeBytes(bytes); // the callback path returns an empty buffer + return 0; +} + /// Free any engine-returned buffer (tiles, style, scamin array, colortables, …). See tile57.h. /// (chart-api.md — the universal free.) export fn tile57_free(ptr: ?*anyopaque, len: usize) callconv(.c) void { diff --git a/src/chart.zig b/src/chart.zig index 330801b..8ad4de6 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -869,7 +869,7 @@ pub const Chart = struct { /// labels + declutter over the whole canvas. Returns PNG bytes (gpa-owned; /// free with freeBytes). Cell-backed sources only; a baked PMTiles source /// has no portrayal to render from (bundle-sourced rendering is future work). - pub fn renderView(self: *Chart, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.MarinerSettings, output: render.pixel.Output) ![]u8 { + pub fn renderView(self: *Chart, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.MarinerSettings, output: render.pixel.Output, cb_table: ?*const render.cb_canvas.CCanvas) ![]u8 { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); @@ -905,6 +905,7 @@ pub const Chart = struct { var ps = render.pixel.PixelSurface.initView(a, &colors, palette, settings, zoom, w, h, pt, @import("tiles").tile.EXTENT); ps.store = store.asStore(); ps.output = output; + ps.cb = cb_table; switch (self.backend) { .reader => |*rd| { diff --git a/src/render/cb_canvas.zig b/src/render/cb_canvas.zig new file mode 100644 index 0000000..b6f2b71 --- /dev/null +++ b/src/render/cb_canvas.zig @@ -0,0 +1,119 @@ +//! cb_canvas.zig — the callback Canvas: a third cv.Canvas implementation +//! (alongside RasterCanvas and the PDF canvas) that forwards each resolved, +//! flattened primitive to a table of C function pointers. This is the seam a C +//! embedder uses to drive its own renderer (GPU or vector) from the same view +//! portrayal, receiving geometry instead of a rasterised PNG. +//! +//! Geometry is emitted in the canvas's PIXEL space (y down) — identical to what +//! RasterCanvas/PdfCanvas receive — and colours are fully resolved for the +//! active palette. Paint order is the endScene-sorted order (the embedder draws +//! calls in the order received; no priority key needed). +const std = @import("std"); +const cv = @import("canvas.zig"); + +// ---- extern C ABI (mirrored in include/tile57.h) --------------------------- +pub const CPoint = extern struct { x: f32, y: f32 }; +pub const CColor = extern struct { r: u8, g: u8, b: u8, a: u8 }; + +/// Multi-ring path: flat vertex array `pts` + `ring_starts[k]` = first vertex +/// index of ring k (ring k spans [ring_starts[k], ring_starts[k+1]), last runs +/// to `n`). Rings are implicitly closed. +pub const CRings = extern struct { + pts: [*]const CPoint, + n: u32, + ring_starts: [*]const u32, + ring_count: u32, +}; + +/// The paint table. Calls arrive in final paint order; `ctx` is passed back +/// verbatim. Coordinates are canvas pixels; widths are pixels. +pub const CCanvas = extern struct { + ctx: ?*anyopaque, + /// Fill closed rings; even_odd != 0 selects the even-odd rule. + fill_path: *const fn (?*anyopaque, *const CRings, CColor, c_int) callconv(.c) void, + /// Stroke polylines `width_px` wide; dash_on/off in px (0,0 = solid). + stroke_path: *const fn (?*anyopaque, *const CRings, f32, f32, f32, CColor) callconv(.c) void, + /// Fill rings with a repeating RGBA8 pattern cell (w*h*4 bytes). + fill_pattern: *const fn (?*anyopaque, *const CRings, u32, u32, [*]const u8) callconv(.c) void, + /// Draw a shaped label as flattened outline rings (canvas px), with an + /// optional halo (halo.a == 0 => none). + draw_glyphs: *const fn (?*anyopaque, *const CRings, CColor, CColor, f32) callconv(.c) void, +}; + +// ---- the Canvas implementation -------------------------------------------- +pub const CbCanvas = struct { + c: *const CCanvas, + a: std.mem.Allocator, + + pub fn init(a: std.mem.Allocator, c: *const CCanvas) CbCanvas { + return .{ .c = c, .a = a }; + } + pub fn asCanvas(self: *CbCanvas) cv.Canvas { + return .{ .ptr = self, .vtable = &vtable }; + } + + const vtable = cv.Canvas.VTable{ + .fillPath = fillPath, + .fillPattern = fillPattern, + .strokePath = strokePath, + .drawGlyphRun = drawGlyphRun, + }; + + fn color(c: cv.Color) CColor { + return .{ .r = c.r, .g = c.g, .b = c.b, .a = c.a }; + } + + // Flatten []const []const Point -> (pts, ring_starts). Arena-allocated; + // the pointers are valid for the duration of the callback. + fn build(self: *CbCanvas, rings: []const []const cv.Point) !struct { CRings, []CPoint, []u32 } { + var n: usize = 0; + for (rings) |r| n += r.len; + const pts = try self.a.alloc(CPoint, n); + const starts = try self.a.alloc(u32, rings.len); + var i: usize = 0; + for (rings, 0..) |r, k| { + starts[k] = @intCast(i); + for (r) |p| { + pts[i] = .{ .x = p.x, .y = p.y }; + i += 1; + } + } + return .{ CRings{ + .pts = pts.ptr, + .n = @intCast(n), + .ring_starts = starts.ptr, + .ring_count = @intCast(rings.len), + }, pts, starts }; + } + + fn fillPath(ptr: *anyopaque, rings: []const []const cv.Point, col: cv.Color, rule: cv.FillRule) anyerror!void { + const self: *CbCanvas = @ptrCast(@alignCast(ptr)); + const built = try self.build(rings); + var cr = built[0]; + self.c.fill_path(self.c.ctx, &cr, color(col), if (rule == .even_odd) 1 else 0); + } + + fn strokePath(ptr: *anyopaque, lines: []const []const cv.Point, width_px: f32, dash: ?[2]f32, col: cv.Color) anyerror!void { + const self: *CbCanvas = @ptrCast(@alignCast(ptr)); + const built = try self.build(lines); + var cr = built[0]; + const on: f32 = if (dash) |d| d[0] else 0; + const off: f32 = if (dash) |d| d[1] else 0; + self.c.stroke_path(self.c.ctx, &cr, width_px, on, off, color(col)); + } + + fn fillPattern(ptr: *anyopaque, rings: []const []const cv.Point, pattern: *const cv.Pattern) anyerror!void { + const self: *CbCanvas = @ptrCast(@alignCast(ptr)); + const built = try self.build(rings); + var cr = built[0]; + self.c.fill_pattern(self.c.ctx, &cr, pattern.w, pattern.h, pattern.rgba.ptr); + } + + fn drawGlyphRun(ptr: *anyopaque, run: *const cv.GlyphRun) anyerror!void { + const self: *CbCanvas = @ptrCast(@alignCast(ptr)); + const built = try self.build(run.rings); + var cr = built[0]; + const halo: CColor = if (run.halo) |h| color(h) else .{ .r = 0, .g = 0, .b = 0, .a = 0 }; + self.c.draw_glyphs(self.c.ctx, &cr, color(run.color), halo, run.halo_w); + } +}; diff --git a/src/render/pixel.zig b/src/render/pixel.zig index abb39f2..59e48f7 100644 --- a/src/render/pixel.zig +++ b/src/render/pixel.zig @@ -25,6 +25,7 @@ const sym = @import("symbols.zig"); const sndfrm = @import("sndfrm.zig"); const fontmod = @import("font.zig"); const pdf = @import("pdf.zig"); +const cb_canvas = @import("cb_canvas.zig"); /// Unmapped color tokens paint magenta, same as the MapLibre style's fallback — /// visible, never silent. @@ -57,7 +58,7 @@ const Op = struct { kind: OpKind, }; -pub const Output = enum { png, pdf }; +pub const Output = enum { png, pdf, callback }; /// A selection HIGHLIGHT overlay painted ON TOP of the finished chart raster — /// the `explore --tui` live cell map sets it so the SELECTED feature stands out @@ -101,8 +102,10 @@ pub const PixelSurface = struct { /// fills/lines-only render). Wired from the sprite module's nanosvg-backed /// store, or a test fake. store: ?sym.SymbolStore = null, - /// endScene output format: raster PNG (default) or vector PDF. + /// endScene output format: raster PNG (default), vector PDF, or callback. output: Output = .png, + /// For output == .callback: the C paint table geometry is forwarded to. + cb: ?*const cb_canvas.CCanvas = null, /// Background colour token the scene clears to (null = "NODTA", the S-52 /// no-data shade, as every whole-scene render does). Overridden only by the /// isolated single-feature thumbnail, which frames one feature on a solid @@ -563,6 +566,23 @@ pub const PixelSurface = struct { try self.drawHighlight(canvas); return pc.finish(out); }, + .callback => { + // Forward every resolved, flattened primitive to the C paint + // table (pixel space, endScene paint order). No bytes returned. + var cc = cb_canvas.CbCanvas.init(self.a, self.cb.?); + const canvas = cc.asCanvas(); + // NODTA background as a full-canvas fill, first (base layer). + const bg = [_]cv.Point{ + .{ .x = 0, .y = 0 }, + .{ .x = @floatFromInt(self.w_px), .y = 0 }, + .{ .x = @floatFromInt(self.w_px), .y = @floatFromInt(self.h_px) }, + .{ .x = 0, .y = @floatFromInt(self.h_px) }, + }; + const bg_rings = [_][]const cv.Point{&bg}; + try canvas.fillPath(&bg_rings, self.resolveColor(self.bg_token orelse "NODTA"), .nonzero); + try self.paintOps(canvas, &dropped); + return out.alloc(u8, 0); + }, } } }; diff --git a/src/render/render.zig b/src/render/render.zig index e2ba0a6..9af114d 100644 --- a/src/render/render.zig +++ b/src/render/render.zig @@ -34,6 +34,7 @@ pub const symbols = @import("symbols.zig"); pub const sndfrm = @import("sndfrm.zig"); pub const font = @import("font.zig"); pub const pdf = @import("pdf.zig"); +pub const cb_canvas = @import("cb_canvas.zig"); test { _ = surface; diff --git a/tools/ascii.zig b/tools/ascii.zig index f7a62ea..50b8aa1 100644 --- a/tools/ascii.zig +++ b/tools/ascii.zig @@ -103,7 +103,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { // Kitty, WezTerm, Konsole): the grid's cell count times the // terminal's cell-pixel size (or the 10x20 guess off-TTY). const cp = cellPx(terminalSize(io)); - const png_bytes = c.renderView(v.lon, v.lat, v.zoom, cols * cp[0], rows * cp[1], palette, &m, .png) catch return usageErr("render failed"); + const png_bytes = c.renderView(v.lon, v.lat, v.zoom, cols * cp[0], rows * cp[1], palette, &m, .png, null) catch return usageErr("render failed"); defer chart.freeBytes(png_bytes); const seq = render.kitty.encodePng(a, png_bytes) catch return usageErr("encode failed"); std.Io.File.stdout().writeStreamingAll(io, seq) catch {}; @@ -205,7 +205,7 @@ fn runAsciiTui(io: std.Io, a: std.mem.Allocator, c: *chart.Chart, lon0: f64, lat r.h = view_h * 3; r.tl_x = vc[0] - @as(f64, @floatFromInt(r.w)) / 2.0; r.tl_y = vc[1] - @as(f64, @floatFromInt(r.h)) / 2.0; - const png_bytes = c.renderView(lon, lat, zoom, r.w, r.h, palette, m, .png) catch break; + const png_bytes = c.renderView(lon, lat, zoom, r.w, r.h, palette, m, .png, null) catch break; const seq = render.kitty.transmitPng(a, png_bytes, r.id) catch break; chart.freeBytes(png_bytes); stdout.writeStreamingAll(io, seq) catch {}; From d3812b1b01c62016c7bb19f341889174ea6a48a2 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 10:14:07 -0400 Subject: [PATCH 02/19] =?UTF-8?q?render:=20add=20VectorSurface=20=E2=80=94?= =?UTF-8?q?=20world-space=20portrayal=20over=20a=20C=20ABI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Surface implementation (src/render/vector.zig) that emits portrayal as a world-space tagged stream over a C callback instead of resolving to pixels: area/line geometry in web-mercator [0,1], point symbols and text as a world anchor + a local reference-px outline, each draw tagged with its feature SCAMIN. This lets a GPU host tessellate once and transform + SCAMIN-cull per frame — constant-size symbols, re-portray-free pan/zoom — like a native SENC. - chart.zig: renderSurfaceView drives VectorSurface over both the .reader (baked pmtiles) and .cell (live .000) backends. - capi.zig: tile57_chart_render_surface_cb export + world/local point/ring structs; tile57.h carries the callback ABI. - vector.zig honours show_inform_callouts for INFORM01 in the live path (the chartstyle.zig gate only applies to the baked-tile style path). Co-Authored-By: Claude Fable 5 --- include/tile57.h | 54 ++++++ src/capi.zig | 35 ++++ src/chart.zig | 68 ++++++++ src/render/render.zig | 2 + src/render/vector.zig | 376 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 535 insertions(+) create mode 100644 src/render/vector.zig diff --git a/include/tile57.h b/include/tile57.h index f6fbfa0..d593ec1 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -295,6 +295,60 @@ int tile57_chart_render_view_cb(tile57_chart *chart, double lon, double lat, dou const struct tile57_mariner *m, const tile57_canvas_cb *canvas); +/* ---- world-space Surface callback: the GPU vector twin ---------------------- + * tile57_chart_render_surface_cb runs the SAME view portrayal as + * tile57_chart_render_view_cb but emits a WORLD-SPACE, semantically TAGGED + * stream rather than resolved pixels: area/line geometry in web-mercator [0,1] + * (y down); point symbols and text as a WORLD anchor + a LOCAL outline in + * reference px (a constant screen size); every draw call tagged with its + * feature's S-57 class and SCAMIN. A GPU host applies its own view transform, + * pins symbols/text at the anchor, and culls by SCAMIN per frame — so pan and + * zoom re-portray NOTHING. Works for a baked bundle (tile replay) or a live + * cell (full S-52 portrayal). See render_surface.md. */ +typedef struct { double x, y; } tile57_world_point; /* web-mercator [0,1], y down */ +typedef struct { float x, y; } tile57_local_point; /* anchor-relative reference px */ + +typedef struct { + const tile57_world_point *pts; uint32_t n; + const uint32_t *ring_starts; uint32_t ring_count; +} tile57_world_rings; +typedef struct { + const tile57_local_point *pts; uint32_t n; + const uint32_t *ring_starts; uint32_t ring_count; +} tile57_local_rings; + +/* The feature the following draw calls belong to. `cls` is the S-57 object-class + * acronym (NUL-terminated; "" if none); `scamin` is the SCAMIN 1:N denominator + * (<= 0 => always visible); `plane` is the S-52 draw priority (paint hint). */ +typedef struct { + const char *cls; + int64_t scamin; + int32_t plane; +} tile57_feature; + +/* Draw table. Pointers are valid only for the duration of the call; ctx is + * passed back verbatim. Calls arrive in Surface emission order (the host owns + * final paint order + label collision). */ +typedef struct { + void *ctx; + /* Filled area (world). even_odd != 0 selects the even-odd rule. */ + void (*fill_area) (void *ctx, const tile57_feature *f, const tile57_world_rings *rings, tile57_rgba color, int even_odd); + /* Stroked line (world); width in reference px, dash on/off px (0,0 solid). */ + void (*stroke_line)(void *ctx, const tile57_feature *f, const tile57_world_rings *lines, float width_px, float dash_on, float dash_off, tile57_rgba color); + /* Point symbol: world anchor + local outline (px). even_odd for compound + * glyphs; stroke_w > 0 => the rings are a polyline stroked stroke_w px wide. */ + void (*draw_symbol)(void *ctx, const tile57_feature *f, tile57_world_point anchor, const tile57_local_rings *rings, tile57_rgba color, int even_odd, float stroke_w); + /* Text: world anchor + local glyph outlines (px, even-odd) + halo + * (halo.a == 0 => none). */ + void (*draw_text) (void *ctx, const tile57_feature *f, tile57_world_point anchor, const tile57_local_rings *glyphs, tile57_rgba color, tile57_rgba halo, float halo_px); +} tile57_surface_cb; + +/* Returns 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. */ +int tile57_chart_render_surface_cb(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const struct tile57_mariner *m, + const tile57_surface_cb *surface); + /* The chart's per-cell metadata as a JSON array, one object per cell: * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", * "issueDate":"20240105","agency":550,"bbox":[west,south,east,north]}, ...] diff --git a/src/capi.zig b/src/capi.zig index 795e95c..c074997 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -475,6 +475,41 @@ export fn tile57_chart_render_view_cb( return 0; } +const CSurface = @import("render").vector.CSurface; + +/// GPU vector twin of render_view_cb: run the SAME view portrayal, but emit a +/// WORLD-SPACE tagged stream (areas/lines in web-mercator [0,1]; symbols/text as +/// a world anchor + local reference-px outline; per-feature class + SCAMIN) to +/// the C surface callback `surface` (see tile57.h). The host transforms geometry +/// and pins symbols/text at a constant screen size, culling by SCAMIN — no +/// re-portrayal per pan/zoom. Works for a baked bundle OR a live cell. +/// 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. +export fn tile57_chart_render_surface_cb( + handle: ?*Chart, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + surface: ?*const CSurface, +) callconv(.c) c_int { + const c = handle orelse return -1; + const sfc = surface orelse return -2; + if (width == 0 or height == 0 or width > 16384 or height > 16384) return -2; + const settings: chartstyle.MarinerSettings = if (m) |p| marinerFromC(p) else .{}; + const palette: RenderPalette = switch (settings.scheme) { + .day => .day, + .dusk => .dusk, + .night => .night, + }; + c.renderSurfaceView(lon, lat, zoom, width, height, palette, &settings, sfc) catch |e| switch (e) { + error.Unsupported => return -3, + else => return -2, + }; + return 0; +} + /// Free any engine-returned buffer (tiles, style, scamin array, colortables, …). See tile57.h. /// (chart-api.md — the universal free.) export fn tile57_free(ptr: ?*anyopaque, len: usize) callconv(.c) void { diff --git a/src/chart.zig b/src/chart.zig index 8ad4de6..1c0ea49 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -943,6 +943,74 @@ pub const Chart = struct { } } + /// renderView's GPU-vector twin: drive a VectorSurface over the same view + /// tiles, emitting a WORLD-SPACE tagged stream to the C surface callback + /// (see render/vector.zig). Live for both a baked bundle (.reader tile + /// replay) and a live cell (.cell portrayal). No bytes are produced. + pub fn renderSurfaceView(self: *Chart, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.MarinerSettings, cb: *const render.vector.CSurface) !void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + + var colors = try render.resolve.Colors.init(a, embedded_assets.colorprofile[0].bytes); + const css_name = switch (palette) { + .day => "daySvgStyle", + .dusk => "duskSvgStyle", + .night => "nightSvgStyle", + }; + var css_data: []const u8 = ""; + for (embedded_assets.css) |e| { + if (std.mem.eql(u8, e.name, css_name)) css_data = e.bytes; + } + const sym_srcs = try a.alloc(sprite.SvgSrc, embedded_assets.symbols.len); + for (embedded_assets.symbols, 0..) |e, i| sym_srcs[i] = .{ .id = e.name, .svg = e.bytes }; + const fill_srcs = try a.alloc(sprite.AreaFillSrc, embedded_assets.areafills.len); + for (embedded_assets.areafills, 0..) |e, i| fill_srcs[i] = .{ .id = e.name, .xml = e.bytes }; + const store = try sprite.CatalogStore.init(a, sym_srcs, fill_srcs, css_data); + defer store.deinit(); + + var ls_srcs = std.ArrayList(@import("assets").LineStyleSrc).empty; + defer ls_srcs.deinit(gpa); + for (embedded_assets.linestyles) |e| ls_srcs.append(gpa, .{ .id = e.name, .xml = e.bytes }) catch {}; + scene.registerLinestylesXml(gpa, ls_srcs.items); + + const pt: f32 = @floatCast(256.0 * std.math.pow(f64, 2.0, zoom - @round(zoom))); + var vs = render.vector.VectorSurface.init(a, &colors, palette, settings, cb); + vs.store = store.asStore(); + const surf = vs.asSurface(); + + var vt = scene.ViewTiles.init(lon, lat, zoom, w, h, pt); + try surf.beginScene(vt.z); + switch (self.backend) { + .reader => |*rd| { + const is_mlt = rd.header.tile_type == .mlt; + while (vt.next()) |t| { + const bytes = (rd.getTile(a, t.z, t.x, t.y) catch continue) orelse continue; + const layers = if (is_mlt) + @import("tiles").mlt.decode(a, bytes) catch continue + else + @import("tiles").mvt.decode(a, bytes) catch continue; + vs.setTile(t.z, t.x, t.y); + scene.replayTile(surf, layers) catch continue; + } + }, + .cell => |*cb2| { + const one = [_]scene.CellRef{.{ + .cell = &cb2.cell, + .portrayal = cb2.portrayal, + .portrayal_plain = cb2.portrayal_plain, + .portrayal_simplified = cb2.portrayal_simplified, + }}; + while (vt.next()) |t| { + vs.setTile(t.z, t.x, t.y); + scene.appendTile(surf, a, &one, t.z, t.x, t.y, self.pick_attrs) catch continue; + } + }, + .cells => return error.Unsupported, + } + _ = try surf.endScene(a); + } + /// Render a VIEW as ASCII art — renderView's shape on the text surface: /// one Unicode character per terminal cell (cols x rows), optional /// ANSI-256 color. Returns UTF-8 bytes, one '\n'-terminated row per grid diff --git a/src/render/render.zig b/src/render/render.zig index 9af114d..7d5236c 100644 --- a/src/render/render.zig +++ b/src/render/render.zig @@ -35,9 +35,11 @@ pub const sndfrm = @import("sndfrm.zig"); pub const font = @import("font.zig"); pub const pdf = @import("pdf.zig"); pub const cb_canvas = @import("cb_canvas.zig"); +pub const vector = @import("vector.zig"); test { _ = surface; + _ = vector; _ = noop; _ = inspect; _ = resolve; diff --git a/src/render/vector.zig b/src/render/vector.zig new file mode 100644 index 0000000..1b3862f --- /dev/null +++ b/src/render/vector.zig @@ -0,0 +1,376 @@ +//! VectorSurface — the Surface implementation for GPU vector embedders. +//! +//! The sibling of PixelSurface: it RESOLVES the same S-52 semantics (token->RGBA, +//! symbol name->outline, text->glyph outlines, the non-SCAMIN display gates) but, +//! instead of baking everything into one pixel frame, emits a WORLD-SPACE tagged +//! stream to a C callback so a GPU host can transform geometry and pin symbols/ +//! text at a constant screen size — no re-portrayal per pan/zoom. +//! +//! Differences from PixelSurface, and why: +//! * Area/line geometry is emitted in web-mercator WORLD coords ([0,1], y down) +//! — the host applies its own view transform each frame. Needs the tile +//! (z,x,y) to unproject tile-space; set via setTile before each tile. +//! * Point symbols and text are emitted as a WORLD anchor + a LOCAL outline in +//! reference px (device scale fixed at 1x, NOT the view's px_per_tile) — so +//! the host draws them at a fixed screen size at the projected anchor. +//! * SCAMIN is NOT gated here; it is passed through per feature so the host can +//! cull per frame. Category / viewing-group / text-group / point / boundary +//! gates DO apply (they track mariner settings, not zoom). We evaluate the +//! combined gate at a high zoom so only SCAMIN is neutralised. +//! * No op buffer / sort / declutter: the host owns paint order (draw calls +//! arrive in Surface emission order) and label collision. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const rs = @import("surface.zig"); +const resolve = @import("resolve.zig"); +const cv = @import("canvas.zig"); +const sym = @import("symbols.zig"); +const sndfrm = @import("sndfrm.zig"); +const fontmod = @import("font.zig"); +const tile = @import("tiles").tile; + +const FALLBACK = cv.Color{ .r = 255, .g = 0, .b = 255 }; +const DASH_ON = 4.0; +const DASH_OFF = 3.0; +/// The zoom the display gates evaluate at — high enough that SCAMIN never gates +/// (log2(DENOM_Z0/scamin) < 28 for any scamin >= 1), so every feature is emitted +/// and the host culls by the per-feature scamin we pass through. +const GATE_ZOOM = 30.0; + +// ---- extern C ABI (mirrored in include/tile57.h) --------------------------- +pub const CWorldPt = extern struct { x: f64, y: f64 }; // web-mercator [0,1], y down +pub const CLocalPt = extern struct { x: f32, y: f32 }; // anchor-relative reference px +pub const CColor = extern struct { r: u8, g: u8, b: u8, a: u8 }; + +/// Multi-ring path in world space (same layout as CRings but f64 world pts). +pub const CWorldRings = extern struct { + pts: [*]const CWorldPt, + n: u32, + ring_starts: [*]const u32, + ring_count: u32, +}; +/// Multi-ring path in anchor-local reference px (symbols/text shapes). +pub const CLocalRings = extern struct { + pts: [*]const CLocalPt, + n: u32, + ring_starts: [*]const u32, + ring_count: u32, +}; + +/// The feature the following draw calls belong to. `cls` is the S-57 object-class +/// acronym (NUL-terminated, "" if none); `scamin` is the SCAMIN 1:N denominator +/// (<=0 => always visible); `plane` is the S-52 draw priority (paint-order hint). +pub const CFeature = extern struct { + cls: [*:0]const u8, + scamin: i64, + plane: i32, +}; + +/// The GPU paint table. Every call gets `ctx` back verbatim; pointers are valid +/// only for the duration of the call. Area/line geometry is world-space; symbol/ +/// text shapes are anchor-local reference px at a world anchor. +pub const CSurface = extern struct { + ctx: ?*anyopaque, + /// Filled area (world). even_odd != 0 selects the even-odd rule. + fill_area: *const fn (?*anyopaque, *const CFeature, *const CWorldRings, CColor, c_int) callconv(.c) void, + /// Stroked line (world), width in reference px; dash on/off px (0,0 = solid). + stroke_line: *const fn (?*anyopaque, *const CFeature, *const CWorldRings, f32, f32, f32, CColor) callconv(.c) void, + /// Point symbol: world anchor + local outline (px). even_odd for compound + /// glyphs; stroke_w > 0 means the rings are a polyline stroked that px wide. + draw_symbol: *const fn (?*anyopaque, *const CFeature, CWorldPt, *const CLocalRings, CColor, c_int, f32) callconv(.c) void, + /// Text label: world anchor + local glyph outlines (px, even-odd) + halo + /// (halo.a == 0 => none). + draw_text: *const fn (?*anyopaque, *const CFeature, CWorldPt, *const CLocalRings, CColor, CColor, f32) callconv(.c) void, +}; + +// ---- the Surface implementation -------------------------------------------- +pub const VectorSurface = struct { + a: Allocator, + colors: *const resolve.Colors, + palette: resolve.PaletteId, + settings: *const resolve.MarinerSettings, + cb: *const CSurface, + store: ?sym.SymbolStore = null, + fnt: ?fontmod.Font = null, + glyph_cache: std.AutoHashMapUnmanaged(u16, []const []const cv.Point) = .empty, + + /// Current tile (set before replaying each tile) for tile-space -> world. + tz: u8 = 0, + tx: u32 = 0, + ty: u32 = 0, + + cur: rs.FeatureMeta = .{}, + cur_visible: bool = true, + + const vtable = rs.Surface.VTable{ + .beginScene = beginScene, + .beginFeature = beginFeature, + .fillArea = fillArea, + .fillPattern = fillPattern, + .strokeLine = strokeLine, + .drawSymbol = drawSymbol, + .drawSounding = drawSounding, + .drawText = drawText, + .endFeature = endFeature, + .endScene = endScene, + }; + + pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, cb: *const CSurface) VectorSurface { + return .{ + .a = a, + .colors = colors, + .palette = palette, + .settings = settings, + .cb = cb, + .fnt = fontmod.Font.init(fontmod.notosans) catch null, + }; + } + + pub fn asSurface(self: *VectorSurface) rs.Surface { + return .{ .ptr = self, .vtable = &vtable }; + } + + pub fn setTile(self: *VectorSurface, z: u8, x: u32, y: u32) void { + self.tz = z; + self.tx = x; + self.ty = y; + } + + fn sp(ctx: *anyopaque) *VectorSurface { + return @ptrCast(@alignCast(ctx)); + } + + fn ccolor(c: cv.Color) CColor { + return .{ .r = c.r, .g = c.g, .b = c.b, .a = c.a }; + } + + fn resolveColor(self: *const VectorSurface, token: []const u8) cv.Color { + const rgb = self.colors.get(self.palette, token) orelse return FALLBACK; + return .{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; + } + + /// Reference device scale: physical multiplier only (px_per_tile fixed at the + /// 256 baseline), so symbol/text px are a constant screen size. + fn refDev(self: *const VectorSurface) f64 { + return self.settings.size_scale; + } + + /// Tile-space point -> web-mercator world [0,1] (y down). + fn worldOf(self: *const VectorSurface, p: rs.TilePoint) CWorldPt { + const n = std.math.pow(f64, 2.0, @floatFromInt(self.tz)); + const e: f64 = @floatFromInt(tile.EXTENT); + return .{ + .x = (@as(f64, @floatFromInt(self.tx)) + @as(f64, @floatFromInt(p.x)) / e) / n, + .y = (@as(f64, @floatFromInt(self.ty)) + @as(f64, @floatFromInt(p.y)) / e) / n, + }; + } + + fn cur_feature(self: *const VectorSurface) CFeature { + // cur.class is a Zig slice; it is NUL-terminated in the meta (acronyms are + // static strings), but guard by duping a sentinel-terminated copy. + const cls = self.a.dupeZ(u8, self.cur.class) catch @as([:0]u8, @constCast("")); + return .{ .cls = cls.ptr, .scamin = self.cur.scamin orelse 0, .plane = @intCast(self.cur.draw_prio) }; + } + + // Flatten tile rings -> world CWorldRings (arena; valid for the call). + fn worldRings(self: *VectorSurface, parts: []const []const rs.TilePoint) !CWorldRings { + var total: usize = 0; + for (parts) |p| total += p.len; + const pts = try self.a.alloc(CWorldPt, total); + const starts = try self.a.alloc(u32, parts.len); + var i: usize = 0; + for (parts, 0..) |part, k| { + starts[k] = @intCast(i); + for (part) |p| { + pts[i] = self.worldOf(p); + i += 1; + } + } + return .{ .pts = pts.ptr, .n = @intCast(total), .ring_starts = starts.ptr, .ring_count = @intCast(parts.len) }; + } + + fn localRings(self: *VectorSurface, parts: []const []const cv.Point) !CLocalRings { + var total: usize = 0; + for (parts) |p| total += p.len; + const pts = try self.a.alloc(CLocalPt, total); + const starts = try self.a.alloc(u32, parts.len); + var i: usize = 0; + for (parts, 0..) |part, k| { + starts[k] = @intCast(i); + for (part) |p| { + pts[i] = .{ .x = p.x, .y = p.y }; + i += 1; + } + } + return .{ .pts = pts.ptr, .n = @intCast(total), .ring_starts = starts.ptr, .ring_count = @intCast(parts.len) }; + } + + // ---- Surface impl --------------------------------------------------------- + fn beginScene(_: *anyopaque, _: u8) anyerror!void {} + fn endFeature(_: *anyopaque) anyerror!void {} + fn endScene(_: *anyopaque, out: Allocator) anyerror![]u8 { + return out.alloc(u8, 0); + } + + fn beginFeature(ctx: *anyopaque, meta: *const rs.FeatureMeta) anyerror!void { + const self = sp(ctx); + self.cur = meta.*; + // Everything but SCAMIN: evaluate at GATE_ZOOM so SCAMIN always passes. + self.cur_visible = resolve.visible(meta, null, GATE_ZOOM, self.settings); + } + + fn fillArea(ctx: *anyopaque, token: rs.ColorToken, rings: []const []const rs.TilePoint, depth: ?rs.DepthRange) anyerror!void { + _ = depth; + const self = sp(ctx); + if (!self.cur_visible) return; + const feat = self.cur_feature(); + var wr = try self.worldRings(rings); + self.cb.fill_area(self.cb.ctx, &feat, &wr, ccolor(self.resolveColor(token)), 0); + } + + fn fillPattern(ctx: *anyopaque, name: rs.SymbolName, rings: []const []const rs.TilePoint) anyerror!void { + _ = name; + const self = sp(ctx); + if (!self.cur_visible) return; + // v1: approximate the S-101 area pattern as a flat translucent tint (the + // host has no pattern raster). A dedicated pattern channel can follow. + const feat = self.cur_feature(); + var wr = try self.worldRings(rings); + self.cb.fill_area(self.cb.ctx, &feat, &wr, .{ .r = 160, .g = 160, .b = 170, .a = 140 }, 0); + } + + fn strokeLine(ctx: *anyopaque, token: rs.ColorToken, width_px: f64, dash: rs.Dash, lines: []const []const rs.TilePoint, valdco: ?f64) anyerror!void { + _ = valdco; // depth-contour value labels: host-side follow-up + const self = sp(ctx); + if (!self.cur_visible) return; + const w: f32 = @floatCast(width_px * self.refDev()); + const on: f32, const off: f32 = switch (dash) { + .solid => .{ 0, 0 }, + .dashed => .{ DASH_ON * w, DASH_OFF * w }, + }; + const feat = self.cur_feature(); + var wr = try self.worldRings(lines); + self.cb.stroke_line(self.cb.ctx, &feat, &wr, w, on, off, ccolor(self.resolveColor(token))); + } + + fn drawSymbol(ctx: *anyopaque, name: rs.SymbolName, at: rs.TilePoint, rot_deg: f64, scale: f64, rot_north: bool, placement: rs.SymbolPlacement, danger_depth: ?f64) anyerror!void { + _ = rot_north; + _ = placement; + const self = sp(ctx); + const store = self.store orelse return; + if (!resolve.visible(&self.cur, name, GATE_ZOOM, self.settings)) return; + // The style path gates INFORM01 information callouts behind + // show_inform_callouts (chartstyle.zig); the live Surface path bypasses + // the style, so mirror that toggle here. + if (!self.settings.show_inform_callouts and std.mem.eql(u8, name, "INFORM01")) return; + var eff = name; + if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; + const s = store.get(eff) orelse return; + try self.emitSymbol(s, at, rot_deg, scale); + } + + fn drawSounding(ctx: *anyopaque, depth_m: f64, swept: bool, low_acc: bool, at: rs.TilePoint) anyerror!void { + const self = sp(ctx); + const store = self.store orelse return; + if (!resolve.visible(&self.cur, null, GATE_ZOOM, self.settings)) return; + const feet = self.settings.depth_unit == .feet; + const shown = if (feet) depth_m * sndfrm.M_TO_FT else depth_m; + const prefix: []const u8 = if (depth_m <= self.settings.safety_depth) "SOUNDS" else "SOUNDG"; + const list = try sndfrm.syms(self.a, prefix, shown, swept, low_acc, feet); + var it = std.mem.splitScalar(u8, list, ','); + while (it.next()) |glyph| { + if (glyph.len == 0) continue; + if (store.get(glyph)) |s| try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); + } + } + + /// Emit one symbol: world anchor + each path's outline in anchor-local + /// reference px (pivot-relative, scaled, rotated). Constant screen size. + fn emitSymbol(self: *VectorSurface, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64) !void { + const anchor = self.worldOf(at); + const feat = self.cur_feature(); + const k: f32 = @floatCast(scale * 100.0 * self.refDev()); + const rad: f32 = @floatCast(rot_deg * std.math.pi / 180.0); + const cosr = @cos(rad); + const sinr = @sin(rad); + for (s.paths) |p| { + const rings = try self.a.alloc([]const cv.Point, p.contours.len); + for (p.contours, 0..) |contour, i| { + const pts = try self.a.alloc(cv.Point, contour.len); + for (contour, 0..) |c, j| { + const lx = (c.x - s.pivot.x) * k; + const ly = (c.y - s.pivot.y) * k; + pts[j] = .{ .x = lx * cosr - ly * sinr, .y = lx * sinr + ly * cosr }; + } + rings[i] = pts; + } + var lr = try self.localRings(rings); + if (p.fill) |color| self.cb.draw_symbol(self.cb.ctx, &feat, anchor, &lr, ccolor(color), 1, 0); + if (p.stroke) |st| self.cb.draw_symbol(self.cb.ctx, &feat, anchor, &lr, ccolor(st.color), 0, st.width * k); + } + } + + fn drawText(ctx: *anyopaque, text: []const u8, style: *const rs.TextStyle, at: rs.TilePoint) anyerror!void { + const self = sp(ctx); + if (!self.cur_visible) return; + if (!resolve.textGroupVisible(style.group, self.settings)) return; + const font_css: f32 = @floatCast(if (style.font_size > 0) style.font_size else 12); + try self.emitText(text, font_css, if (style.halign.len > 0) style.halign else "center", if (style.valign.len > 0) style.valign else "middle", @floatCast(style.offset_x), @floatCast(style.offset_y), self.resolveColor(style.color), at); + } + + fn glyphOutline(self: *VectorSurface, gid: u16) ![]const []const cv.Point { + if (self.glyph_cache.get(gid)) |hit| return hit; + const out = try self.fnt.?.outline(self.a, gid); + try self.glyph_cache.put(self.a, gid, out); + return out; + } + + /// Shape a label into anchor-local reference px glyph outlines (baseline at + /// the origin, aligned per halign/valign with mm offsets) at a world anchor. + fn emitText(self: *VectorSurface, text: []const u8, font_css: f32, halign: []const u8, valign: []const u8, ox_mm: f32, oy_mm: f32, color: cv.Color, at: rs.TilePoint) !void { + const f = &(self.fnt orelse return); + const px = font_css * @as(f32, @floatCast(self.refDev())); + if (px <= 1) return; + const mm_px: f32 = @floatCast(sndfrm.SYMBOL_SCALE * 100.0 * self.refDev()); + + var gids = std.ArrayList(struct { gid: u16, x: f32 }).empty; + var pen: f32 = 0; + var it = (std.unicode.Utf8View.init(text) catch return).iterator(); + while (it.nextCodepoint()) |cp| { + const gid = f.glyphIndex(cp); + try gids.append(self.a, .{ .gid = gid, .x = pen }); + pen += f.advance(gid) * px; + } + if (gids.items.len == 0) return; + + var x0: f32 = ox_mm * mm_px; + if (std.mem.eql(u8, halign, "center")) x0 -= pen / 2; + if (std.mem.eql(u8, halign, "right")) x0 -= pen; + var baseline: f32 = oy_mm * mm_px; + if (std.mem.eql(u8, valign, "top")) { + baseline += f.ascent * px; + } else if (std.mem.eql(u8, valign, "middle")) { + baseline += (f.ascent - f.descent) / 2 * px; + } else { + baseline -= f.descent * px; + } + + var rings = std.ArrayList([]const cv.Point).empty; + for (gids.items) |g| { + const contours = try self.glyphOutline(g.gid); + for (contours) |contour| { + const pts = try self.a.alloc(cv.Point, contour.len); + for (contour, 0..) |p, i| { + // em units, y up -> local px, y down (anchor-relative). + pts[i] = .{ .x = x0 + g.x + p.x * px, .y = baseline - p.y * px }; + } + try rings.append(self.a, pts); + } + } + if (rings.items.len == 0) return; + const feat = self.cur_feature(); + var lr = try self.localRings(rings.items); + self.cb.draw_text(self.cb.ctx, &feat, self.worldOf(at), &lr, ccolor(color), .{ .r = 0, .g = 0, .b = 0, .a = 0 }, 0); + } +}; From 619e5283e2305f3952efc34ef5d078ecee8b6a95 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 11:17:18 -0400 Subject: [PATCH 03/19] render: draw symbols/soundings as atlas sprites over the C ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a draw_sprite callback to the VectorSurface vtable. When the host sets it, point symbols and sounding glyphs emit as a symbol name + world anchor + rotation + pivot-relative half-extent (reference px) instead of tessellated outlines, so the host draws them as pre-rendered atlas quads — antialiased and far cheaper than tessellation. The pivot-centred atlas cell drawn centred on the anchor reproduces the vector placement exactly, including multi-glyph soundings whose per-glyph pivots lay out the number. - vector.zig: emitSprite helper shared by drawSymbol + drawSounding; a null draw_sprite falls back to tessellating the outline. - capi.zig: tile57_bake_sprite_mln returns the MapLibre sprite-mln atlas (pivot-centred cells + {name:{x,y,width,height,pixelRatio}} JSON) from the embedded catalogue. Co-Authored-By: Claude Fable 5 --- include/tile57.h | 10 ++++++++++ src/capi.zig | 21 +++++++++++++++++++++ src/render/vector.zig | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/include/tile57.h b/include/tile57.h index d593ec1..181ba52 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -192,6 +192,10 @@ typedef struct { uint8_t *pattern_json; size_t pattern_json_len; uint8_t *pattern_png; size_t pattern_png_len; } tile57_assets; int tile57_bake_assets(const char *catalog_dir, tile57_assets *out); +/* Like tile57_bake_assets but sprite_json/sprite_png carry the MapLibre sprite-mln + * atlas (pivot-centred cells + {name:{x,y,width,height,pixelRatio}} JSON); other + * fields are NULL. Free with tile57_assets_free. 1=ok, 0=error. */ +int tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out); void tile57_assets_free(tile57_assets *out); /* Release a chart and all cached tiles. Must not be called while any renderer @@ -341,6 +345,12 @@ typedef struct { /* Text: world anchor + local glyph outlines (px, even-odd) + halo * (halo.a == 0 => none). */ void (*draw_text) (void *ctx, const tile57_feature *f, tile57_world_point anchor, const tile57_local_rings *glyphs, tile57_rgba color, tile57_rgba halo, float halo_px); + /* Point symbol as a sprite: symbol name (ptr,len) to look up in the atlas + * (tile57_bake_assets sprite_png/json), world anchor, rotation (deg), and the + * symbol's un-rotated half-extent in reference px. Draw the atlas cell as a + * quad of that half-size, centred on the anchor. NULL => symbols tessellate + * via draw_symbol instead. Must be the LAST field (ABI-appended). */ + void (*draw_sprite)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, tile57_world_point anchor, float rot_deg, float half_w_px, float half_h_px); } tile57_surface_cb; /* Returns 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. */ diff --git a/src/capi.zig b/src/capi.zig index c074997..0370a9f 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -617,6 +617,27 @@ export fn tile57_bake_assets(catalog_dir: ?[*:0]const u8, out: *CAssets) callcon return 1; } +/// Like tile57_bake_assets but the sprite_* fields carry the MapLibre sprite-mln +/// atlas: each symbol pivot-centred in its cell + {name:{x,y,width,height, +/// pixelRatio}} JSON. Only sprite_json/sprite_png are filled. Free with +/// tile57_assets_free. 1=ok, 0=error. See tile57.h. +export fn tile57_bake_sprite_mln(catalog_dir: ?[*:0]const u8, out: *CAssets) callconv(.c) c_int { + out.* = .{}; + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const cd = spanOpt(catalog_dir) orelse ""; + const spr = bundle.spriteMlnBytes(io, a, cd, bundle.DEFAULT_CSS, &[_][]const u8{}) catch return 0; + fillAssets(out, "", "", spr.json, spr.png, "", "") catch { + tile57_assets_free(out); + return 0; + }; + return 1; +} + /// Free every non-null buffer in *out and zero the struct. See tile57.h. export fn tile57_assets_free(out: *CAssets) callconv(.c) void { if (out.colortables) |p| chart.freeBytes(p[0..out.colortables_len]); diff --git a/src/render/vector.zig b/src/render/vector.zig index 1b3862f..a3bdf03 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -82,6 +82,11 @@ pub const CSurface = extern struct { /// Text label: world anchor + local glyph outlines (px, even-odd) + halo /// (halo.a == 0 => none). draw_text: *const fn (?*anyopaque, *const CFeature, CWorldPt, *const CLocalRings, CColor, CColor, f32) callconv(.c) void, + /// Point symbol as a sprite: name (ptr,len) to look up in the atlas, world + /// anchor, rotation (deg), and the symbol's un-rotated half-extent in + /// reference px (draw the atlas cell as a quad of that half-size, centred on + /// the anchor). Null => symbols tessellate via draw_symbol instead. + draw_sprite: ?*const fn (?*anyopaque, *const CFeature, [*]const u8, usize, CWorldPt, f32, f32, f32) callconv(.c) void = null, }; // ---- the Surface implementation -------------------------------------------- @@ -267,7 +272,29 @@ pub const VectorSurface = struct { var eff = name; if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; const s = store.get(eff) orelse return; - try self.emitSymbol(s, at, rot_deg, scale); + // Draw as an atlas sprite when the host supports it; else tessellate. + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale) + else try self.emitSymbol(s, at, rot_deg, scale); + } + + /// Emit a symbol as an atlas sprite: pass its un-rotated pivot-relative + /// half-extent (reference px) + world anchor + rotation. The pivot-centred + /// atlas cell drawn centred on the anchor reproduces the vector placement — + /// for point symbols AND multi-glyph soundings, whose per-glyph pivots lay + /// out the number. + fn emitSprite(self: *VectorSurface, draw_sprite: anytype, name: []const u8, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64) void { + const k = scale * 100.0 * self.refDev(); + var hw: f64 = 0; + var hh: f64 = 0; + for (s.paths) |p| for (p.contours) |contour| for (contour) |c| { + const lx = @abs((c.x - s.pivot.x) * k); + const ly = @abs((c.y - s.pivot.y) * k); + if (lx > hw) hw = lx; + if (ly > hh) hh = ly; + }; + if (hw <= 0 or hh <= 0) return; + const feat = self.cur_feature(); + draw_sprite(self.cb.ctx, &feat, name.ptr, name.len, self.worldOf(at), @floatCast(rot_deg), @floatCast(hw), @floatCast(hh)); } fn drawSounding(ctx: *anyopaque, depth_m: f64, swept: bool, low_acc: bool, at: rs.TilePoint) anyerror!void { @@ -281,7 +308,9 @@ pub const VectorSurface = struct { var it = std.mem.splitScalar(u8, list, ','); while (it.next()) |glyph| { if (glyph.len == 0) continue; - if (store.get(glyph)) |s| try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); + const s = store.get(glyph) orelse continue; + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE) + else try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); } } From 9b238a77115fb74dd35d38e9c75ccfdbb42e52ed Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 11:27:30 -0400 Subject: [PATCH 04/19] render: tile area fill patterns via a draw_pattern callback Add draw_pattern to the VectorSurface vtable: fillPattern emits the pattern name + world rings so the host can tile the real S-101 pattern cell across the polygon (from the sprite-mln atlas's pat: entries) at a constant screen size instead of a flat tint. A null draw_pattern keeps the flat-tint fallback. Co-Authored-By: Claude Fable 5 --- include/tile57.h | 4 ++++ src/render/vector.zig | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/include/tile57.h b/include/tile57.h index 181ba52..bf70f3c 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -351,6 +351,10 @@ typedef struct { * quad of that half-size, centred on the anchor. NULL => symbols tessellate * via draw_symbol instead. Must be the LAST field (ABI-appended). */ void (*draw_sprite)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, tile57_world_point anchor, float rot_deg, float half_w_px, float half_h_px); + /* Area fill pattern: pattern name (ptr,len) to look up in the atlas ("pat:" + * prefix) + the fill rings (world). Tile the cell across the polygon at a + * constant screen size. NULL => flat tint. Must be the LAST field. */ + void (*draw_pattern)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, const tile57_world_rings *rings); } tile57_surface_cb; /* Returns 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. */ diff --git a/src/render/vector.zig b/src/render/vector.zig index a3bdf03..cbb446b 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -87,6 +87,10 @@ pub const CSurface = extern struct { /// reference px (draw the atlas cell as a quad of that half-size, centred on /// the anchor). Null => symbols tessellate via draw_symbol instead. draw_sprite: ?*const fn (?*anyopaque, *const CFeature, [*]const u8, usize, CWorldPt, f32, f32, f32) callconv(.c) void = null, + /// Area fill pattern: pattern name (ptr,len) to look up in the atlas ("pat:" + /// prefix) + the fill rings (world). Tile the pattern cell across the polygon + /// at a constant screen size. Null => patterns fall back to a flat tint. + draw_pattern: ?*const fn (?*anyopaque, *const CFeature, [*]const u8, usize, *const CWorldRings) callconv(.c) void = null, }; // ---- the Surface implementation -------------------------------------------- @@ -235,14 +239,17 @@ pub const VectorSurface = struct { } fn fillPattern(ctx: *anyopaque, name: rs.SymbolName, rings: []const []const rs.TilePoint) anyerror!void { - _ = name; const self = sp(ctx); if (!self.cur_visible) return; - // v1: approximate the S-101 area pattern as a flat translucent tint (the - // host has no pattern raster). A dedicated pattern channel can follow. const feat = self.cur_feature(); var wr = try self.worldRings(rings); - self.cb.fill_area(self.cb.ctx, &feat, &wr, .{ .r = 160, .g = 160, .b = 170, .a = 140 }, 0); + // Tile the real S-101 pattern cell when the host supports it; else fall + // back to a flat translucent tint. + if (self.cb.draw_pattern) |dp| { + dp(self.cb.ctx, &feat, name.ptr, name.len, &wr); + } else { + self.cb.fill_area(self.cb.ctx, &feat, &wr, .{ .r = 160, .g = 160, .b = 170, .a = 140 }, 0); + } } fn strokeLine(ctx: *anyopaque, token: rs.ColorToken, width_px: f64, dash: rs.Dash, lines: []const []const rs.TilePoint, valdco: ?f64) anyerror!void { From df6ee94b8b4a82b9a3b30ffec4459fb2c9124c05 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 12:06:51 -0400 Subject: [PATCH 05/19] render: declutter labels/symbols behind the VectorSurface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a greedy screen-box occupancy pass (Declutter) driven off the engine's draw-priority feature order, so the first (highest-priority) box placed wins — the S-52 collision rule. Behind the VectorSurface so it's a shared feature other surfaces (PixelSurface) can adopt in place of their own copy. - Navaid point symbols place unconditionally and reserve their box. - Soundings declutter as a whole number (all digits or none), thinning the crowding at low zoom. - Text labels yield when their estimated box collides. - Boxes are screen px at the portray zoom (VectorSurface.view_zoom). Co-Authored-By: Claude Fable 5 --- src/chart.zig | 1 + src/render/vector.zig | 70 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/chart.zig b/src/chart.zig index 1c0ea49..5d4a6d5 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -977,6 +977,7 @@ pub const Chart = struct { const pt: f32 = @floatCast(256.0 * std.math.pow(f64, 2.0, zoom - @round(zoom))); var vs = render.vector.VectorSurface.init(a, &colors, palette, settings, cb); vs.store = store.asStore(); + vs.view_zoom = zoom; // scale at which labels/symbols declutter const surf = vs.asSurface(); var vt = scene.ViewTiles.init(lon, lat, zoom, w, h, pt); diff --git a/src/render/vector.zig b/src/render/vector.zig index cbb446b..4e543b7 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -93,6 +93,42 @@ pub const CSurface = extern struct { draw_pattern: ?*const fn (?*anyopaque, *const CFeature, [*]const u8, usize, *const CWorldRings) callconv(.c) void = null, }; +/// Greedy screen-box occupancy for label/symbol declutter. Features arrive in +/// draw-priority order, so placing the highest priority first and skipping +/// lower-priority overlaps IS the S-52 collision rule. Boxes are screen px at +/// the portray zoom. Shared so other surfaces can adopt it (dropping their own). +pub const Declutter = struct { + cells: std.AutoHashMapUnmanaged(u64, void) = .empty, + const CELL: f64 = 8.0; + fn cellKey(cx: i32, cy: i32) u64 { + return (@as(u64, @as(u32, @bitCast(cx))) << 32) | @as(u64, @as(u32, @bitCast(cy))); + } + /// Reserve a box centred at (sx,sy) px, half-extent (hw,hh). `force` places + /// unconditionally (and blocks later boxes); else returns false on a + /// collision (caller skips drawing) — the higher-priority box already there. + pub fn place(self: *Declutter, a: Allocator, sx: f64, sy: f64, hw: f64, hh: f64, force: bool) bool { + const x0: i32 = @intFromFloat(@floor((sx - hw) / CELL)); + const x1: i32 = @intFromFloat(@floor((sx + hw) / CELL)); + const y0: i32 = @intFromFloat(@floor((sy - hh) / CELL)); + const y1: i32 = @intFromFloat(@floor((sy + hh) / CELL)); + if (!force) { + var y = y0; + while (y <= y1) : (y += 1) { + var x = x0; + while (x <= x1) : (x += 1) + if (self.cells.contains(cellKey(x, y))) return false; + } + } + var y = y0; + while (y <= y1) : (y += 1) { + var x = x0; + while (x <= x1) : (x += 1) + self.cells.put(a, cellKey(x, y), {}) catch {}; + } + return true; + } +}; + // ---- the Surface implementation -------------------------------------------- pub const VectorSurface = struct { a: Allocator, @@ -112,6 +148,11 @@ pub const VectorSurface = struct { cur: rs.FeatureMeta = .{}, cur_visible: bool = true, + /// Portray zoom (set by the driver) — the scale at which labels/symbols are + /// decluttered, and the shared occupancy grid. + view_zoom: f64 = 0, + declutter: Declutter = .{}, + const vtable = rs.Surface.VTable{ .beginScene = beginScene, .beginFeature = beginFeature, @@ -280,7 +321,9 @@ pub const VectorSurface = struct { if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; const s = store.get(eff) orelse return; // Draw as an atlas sprite when the host supports it; else tessellate. - if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale) + // Navaid point symbols place unconditionally (force) — they anchor the + // declutter and lower-priority soundings/text yield around them. + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale, true) else try self.emitSymbol(s, at, rot_deg, scale); } @@ -289,7 +332,7 @@ pub const VectorSurface = struct { /// atlas cell drawn centred on the anchor reproduces the vector placement — /// for point symbols AND multi-glyph soundings, whose per-glyph pivots lay /// out the number. - fn emitSprite(self: *VectorSurface, draw_sprite: anytype, name: []const u8, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64) void { + fn emitSprite(self: *VectorSurface, draw_sprite: anytype, name: []const u8, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64, force: bool) void { const k = scale * 100.0 * self.refDev(); var hw: f64 = 0; var hh: f64 = 0; @@ -300,8 +343,13 @@ pub const VectorSurface = struct { if (ly > hh) hh = ly; }; if (hw <= 0 or hh <= 0) return; + const anchor = self.worldOf(at); + // Declutter: navaid symbols place unconditionally (force) and block; a + // sounding yields if its box is already taken. + const scale_px = 256.0 * std.math.exp2(self.view_zoom); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, hw, hh, force)) return; const feat = self.cur_feature(); - draw_sprite(self.cb.ctx, &feat, name.ptr, name.len, self.worldOf(at), @floatCast(rot_deg), @floatCast(hw), @floatCast(hh)); + draw_sprite(self.cb.ctx, &feat, name.ptr, name.len, anchor, @floatCast(rot_deg), @floatCast(hw), @floatCast(hh)); } fn drawSounding(ctx: *anyopaque, depth_m: f64, swept: bool, low_acc: bool, at: rs.TilePoint) anyerror!void { @@ -312,11 +360,18 @@ pub const VectorSurface = struct { const shown = if (feet) depth_m * sndfrm.M_TO_FT else depth_m; const prefix: []const u8 = if (depth_m <= self.settings.safety_depth) "SOUNDS" else "SOUNDG"; const list = try sndfrm.syms(self.a, prefix, shown, swept, low_acc, feet); + // Declutter the WHOLE sounding as one box (so a digit of a number is + // never dropped on its own); a crowded-out sounding is skipped entirely. + const anchor = self.worldOf(at); + const scale_px = 256.0 * std.math.exp2(self.view_zoom); + const rd = self.refDev(); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, 13.0 * rd, 8.0 * rd, false)) return; var it = std.mem.splitScalar(u8, list, ','); while (it.next()) |glyph| { if (glyph.len == 0) continue; const s = store.get(glyph) orelse continue; - if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE) + // force: the sounding as a whole already passed declutter. + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE, true) else try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); } } @@ -352,6 +407,13 @@ pub const VectorSurface = struct { if (!self.cur_visible) return; if (!resolve.textGroupVisible(style.group, self.settings)) return; const font_css: f32 = @floatCast(if (style.font_size > 0) style.font_size else 12); + // Declutter: estimate the label box (width ~ chars × 0.55 em) and yield + // if it collides with a higher-priority label/symbol already placed. + const px = @as(f64, font_css) * self.refDev(); + const w = @as(f64, @floatFromInt(text.len)) * px * 0.55; + const anchor = self.worldOf(at); + const scale_px = 256.0 * std.math.exp2(self.view_zoom); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, w * 0.5, px * 0.6, false)) return; try self.emitText(text, font_css, if (style.halign.len > 0) style.halign else "center", if (style.valign.len > 0) style.valign else "middle", @floatCast(style.offset_x), @floatCast(style.offset_y), self.resolveColor(style.color), at); } From d922b72e9a23c984f7bb9fb0cf872fa3f40b6507 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 12:32:36 -0400 Subject: [PATCH 06/19] render: draw complex-linestyle bricks at native size so they tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line-placed symbols (SymbolPlacement.line) are a repeating pattern the engine lays down at a native tile-space spacing. Sizing them by the display refDev (the "2x density" fix) inflated them past that spacing, so they overlapped and smashed together. Draw .line marks at NATIVE size (dev 1.0) so they tile like bricks, and skip declutter for them — they're an even pattern, not point labels. Point navaids and soundings keep the display size + declutter. Co-Authored-By: Claude Fable 5 --- src/render/vector.zig | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/render/vector.zig b/src/render/vector.zig index 4e543b7..9ba7e12 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -309,7 +309,6 @@ pub const VectorSurface = struct { fn drawSymbol(ctx: *anyopaque, name: rs.SymbolName, at: rs.TilePoint, rot_deg: f64, scale: f64, rot_north: bool, placement: rs.SymbolPlacement, danger_depth: ?f64) anyerror!void { _ = rot_north; - _ = placement; const self = sp(ctx); const store = self.store orelse return; if (!resolve.visible(&self.cur, name, GATE_ZOOM, self.settings)) return; @@ -321,10 +320,13 @@ pub const VectorSurface = struct { if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; const s = store.get(eff) orelse return; // Draw as an atlas sprite when the host supports it; else tessellate. - // Navaid point symbols place unconditionally (force) — they anchor the - // declutter and lower-priority soundings/text yield around them. - if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale, true) - else try self.emitSymbol(s, at, rot_deg, scale); + // A .point navaid draws at display size (refDev) and force-places into + // the declutter. A .line brick draws at NATIVE size (dev 1.0) so it + // tiles at the engine's tile-space spacing, and is not decluttered. + if (self.cb.draw_sprite) |ds| { + if (placement == .point) self.emitSprite(ds, eff, s, at, rot_deg, scale, self.refDev(), true) + else self.emitSprite(ds, eff, s, at, rot_deg, scale, 1.0, null); + } else try self.emitSymbol(s, at, rot_deg, scale); } /// Emit a symbol as an atlas sprite: pass its un-rotated pivot-relative @@ -332,8 +334,12 @@ pub const VectorSurface = struct { /// atlas cell drawn centred on the anchor reproduces the vector placement — /// for point symbols AND multi-glyph soundings, whose per-glyph pivots lay /// out the number. - fn emitSprite(self: *VectorSurface, draw_sprite: anytype, name: []const u8, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64, force: bool) void { - const k = scale * 100.0 * self.refDev(); + /// `dev` = the device scale for the symbol size (refDev for display-sized + /// point symbols; 1.0 for line bricks, which must match the engine's native + /// tile-space spacing so they tile). `declut` null = no declutter (line + /// patterns), else place with that `force`. + fn emitSprite(self: *VectorSurface, draw_sprite: anytype, name: []const u8, s: *const sym.Symbol, at: rs.TilePoint, rot_deg: f64, scale: f64, dev: f64, declut: ?bool) void { + const k = scale * 100.0 * dev; var hw: f64 = 0; var hh: f64 = 0; for (s.paths) |p| for (p.contours) |contour| for (contour) |c| { @@ -344,10 +350,10 @@ pub const VectorSurface = struct { }; if (hw <= 0 or hh <= 0) return; const anchor = self.worldOf(at); - // Declutter: navaid symbols place unconditionally (force) and block; a - // sounding yields if its box is already taken. - const scale_px = 256.0 * std.math.exp2(self.view_zoom); - if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, hw, hh, force)) return; + if (declut) |force| { + const scale_px = 256.0 * std.math.exp2(self.view_zoom); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, hw, hh, force)) return; + } const feat = self.cur_feature(); draw_sprite(self.cb.ctx, &feat, name.ptr, name.len, anchor, @floatCast(rot_deg), @floatCast(hw), @floatCast(hh)); } @@ -371,7 +377,7 @@ pub const VectorSurface = struct { if (glyph.len == 0) continue; const s = store.get(glyph) orelse continue; // force: the sounding as a whole already passed declutter. - if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE, true) + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE, self.refDev(), true) else try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); } } From 15b6c56890381d71bad4dff39b6d54595bf7ca6d Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 13:07:48 -0400 Subject: [PATCH 07/19] render: declutter text only, per S-52 (not symbols/soundings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the spec (as tile57's PixelSurface already does): only text participates in the collision declutter — symbols are icon-allow-overlap and soundings are symbols, so both always draw (density governed by SCAMIN). The first cut also decluttered symbols (force-reserving boxes that blocked nearby labels) and soundings, which was far too aggressive. Now navaids/soundings all draw and only overlapping text yields, highest draw-priority first (emission order). Co-Authored-By: Claude Fable 5 --- src/render/vector.zig | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/render/vector.zig b/src/render/vector.zig index 9ba7e12..14c0b2b 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -320,13 +320,12 @@ pub const VectorSurface = struct { if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; const s = store.get(eff) orelse return; // Draw as an atlas sprite when the host supports it; else tessellate. - // A .point navaid draws at display size (refDev) and force-places into - // the declutter. A .line brick draws at NATIVE size (dev 1.0) so it - // tiles at the engine's tile-space spacing, and is not decluttered. - if (self.cb.draw_sprite) |ds| { - if (placement == .point) self.emitSprite(ds, eff, s, at, rot_deg, scale, self.refDev(), true) - else self.emitSprite(ds, eff, s, at, rot_deg, scale, 1.0, null); - } else try self.emitSymbol(s, at, rot_deg, scale); + // Symbols never participate in declutter (S-52 icon-allow-overlap — they + // all draw). A .point navaid draws at display size (refDev); a .line + // brick draws at NATIVE size (dev 1.0) so it tiles at the engine spacing. + const dev: f64 = if (placement == .point) self.refDev() else 1.0; + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale, dev, null) + else try self.emitSymbol(s, at, rot_deg, scale); } /// Emit a symbol as an atlas sprite: pass its un-rotated pivot-relative @@ -366,18 +365,13 @@ pub const VectorSurface = struct { const shown = if (feet) depth_m * sndfrm.M_TO_FT else depth_m; const prefix: []const u8 = if (depth_m <= self.settings.safety_depth) "SOUNDS" else "SOUNDG"; const list = try sndfrm.syms(self.a, prefix, shown, swept, low_acc, feet); - // Declutter the WHOLE sounding as one box (so a digit of a number is - // never dropped on its own); a crowded-out sounding is skipped entirely. - const anchor = self.worldOf(at); - const scale_px = 256.0 * std.math.exp2(self.view_zoom); - const rd = self.refDev(); - if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, 13.0 * rd, 8.0 * rd, false)) return; + // Soundings are symbols (S-52 icon-allow-overlap): they do NOT declutter; + // density is governed by SCAMIN. Each digit self-positions by its pivot. var it = std.mem.splitScalar(u8, list, ','); while (it.next()) |glyph| { if (glyph.len == 0) continue; const s = store.get(glyph) orelse continue; - // force: the sounding as a whole already passed declutter. - if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE, self.refDev(), true) + if (self.cb.draw_sprite) |ds| self.emitSprite(ds, glyph, s, at, 0, sndfrm.SYMBOL_SCALE, self.refDev(), null) else try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); } } From 0f530b18b4d81bfc9eb71b8995081eceab3a41b2 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 13:26:52 -0400 Subject: [PATCH 08/19] render: cursor object-query (S-52 pick) over the C ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tile57_chart_query(lon,lat,cb): replays the finest tile covering the point through a QuerySurface that tests each feature (area point-in-polygon; line/point within a small radius) and reports the hits' S-57 class + attribute JSON + source cell via a callback — everything the §10.8 pick report needs, straight off the FeatureMeta contract, no S-57 decode in the host. - render/query.zig: QuerySurface (point-in-feature Surface backend). - chart.zig: Chart.queryPoint drives it over the reader (baked) and cell (live) backends. - scene.zig: metaFromProps now reads the baked s57/cell tile properties into FeatureMeta (the reader path was dropping them, so a baked chart's query returned class but empty attributes). Co-Authored-By: Claude Fable 5 --- include/tile57.h | 13 ++++ src/capi.zig | 12 ++++ src/chart.zig | 47 +++++++++++++++ src/render/query.zig | 134 ++++++++++++++++++++++++++++++++++++++++++ src/render/render.zig | 1 + src/scene/scene.zig | 2 + 6 files changed, 209 insertions(+) create mode 100644 src/render/query.zig diff --git a/include/tile57.h b/include/tile57.h index bf70f3c..86bc37f 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -363,6 +363,19 @@ int tile57_chart_render_surface_cb(tile57_chart *chart, double lon, double lat, const struct tile57_mariner *m, const tile57_surface_cb *surface); +/* Cursor object-query (S-52 pick): feature() is invoked once per feature the + * point (lon,lat) falls in — area point-in-polygon, line/point within a small + * radius — with the S-57 object-class acronym, the attribute JSON (acronym->value), + * and the source cell name. Pointers are valid only for the duration of the call. */ +typedef struct { + void *ctx; + void (*feature)(void *ctx, const char *cls, size_t cls_len, + const char *s57, size_t s57_len, + const char *cell, size_t cell_len); +} tile57_query_cb; +/* Returns 0 ok, -1 bad args. */ +int tile57_chart_query(tile57_chart *chart, double lon, double lat, const tile57_query_cb *cb); + /* The chart's per-cell metadata as a JSON array, one object per cell: * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", * "issueDate":"20240105","agency":550,"bbox":[west,south,east,north]}, ...] diff --git a/src/capi.zig b/src/capi.zig index 0370a9f..93c6a35 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -148,6 +148,18 @@ export fn tile57_chart_get_info(src: ?*Chart, out: *CChartInfo) callconv(.c) voi } } +const CQueryCb = @import("render").query.QueryCb; + +/// Cursor object-query at (lon,lat): invokes cb->feature once per feature the +/// point falls in, with its S-57 class, attribute JSON, and source cell. +/// 0=ok, -1=bad args. See tile57.h. +export fn tile57_chart_query(handle: ?*Chart, lon: f64, lat: f64, cb: ?*const CQueryCb) callconv(.c) c_int { + const self = handle orelse return -1; + const cbp = cb orelse return -1; + self.queryPoint(lon, lat, cbp) catch return -1; + return 0; +} + // Progress callback for tile57_bake_pmtiles / tile57_bake_bundle (matches the header // typedef + chart.Progress + bake_enc.Progress). const BakeProgress = ?*const fn (user: ?*anyopaque, stage: u8, done: usize, total: usize, band_index: u8, band_count: u8, band_name: ?[*:0]const u8) callconv(.c) void; diff --git a/src/chart.zig b/src/chart.zig index 5d4a6d5..3f5811a 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -1012,6 +1012,53 @@ pub const Chart = struct { _ = try surf.endScene(a); } + /// Cursor object-query: replay the finest tile covering (lon,lat) through a + /// QuerySurface and report each feature the point falls in (class + S-57 + /// attribute JSON + source cell) via `cb`. Used for the S-52 §10.8 pick. + pub fn queryPoint(self: *Chart, lon: f64, lat: f64, cb: *const render.query.QueryCb) !void { + const t = @import("tiles").tile; + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const z: u8 = @intCast(self.zoomRange().max); + const world = t.lonLatToWorld(lon, lat); + const n = std.math.exp2(@as(f64, @floatFromInt(z))); + const tx: u32 = @intFromFloat(@floor(world[0] * n)); + const ty: u32 = @intFromFloat(@floor(world[1] * n)); + const local = t.project(lon, lat, z, tx, ty, t.EXTENT); + var qs = render.query.QuerySurface{ + .qx = @floatFromInt(local.x), + .qy = @floatFromInt(local.y), + .radius = 96.0, // ~6 px at native tile scale + .cb = cb, + }; + const surf = qs.asSurface(); + try surf.beginScene(z); + switch (self.backend) { + .reader => |*rd| { + const is_mlt = rd.header.tile_type == .mlt; + const bytes = (rd.getTile(a, z, tx, ty) catch return) orelse return; + if (bytes.len == 0) return; + const layers = if (is_mlt) + @import("tiles").mlt.decode(a, bytes) catch return + else + @import("tiles").mvt.decode(a, bytes) catch return; + scene.replayTile(surf, layers) catch return; + }, + .cell => |*cb2| { + const one = [_]scene.CellRef{.{ + .cell = &cb2.cell, + .portrayal = cb2.portrayal, + .portrayal_plain = cb2.portrayal_plain, + .portrayal_simplified = cb2.portrayal_simplified, + }}; + scene.appendTile(surf, a, &one, z, tx, ty, self.pick_attrs) catch return; + }, + .cells => return, + } + _ = surf.endScene(a) catch {}; + } + /// Render a VIEW as ASCII art — renderView's shape on the text surface: /// one Unicode character per terminal cell (cols x rows), optional /// ANSI-256 color. Returns UTF-8 bytes, one '\n'-terminated row per grid diff --git a/src/render/query.zig b/src/render/query.zig new file mode 100644 index 0000000..8c03136 --- /dev/null +++ b/src/render/query.zig @@ -0,0 +1,134 @@ +//! QuerySurface: a Surface backend for cursor object-query (S-52 §10.8 pick). +//! Given a point in a tile's local coordinates, it replays that tile and records +//! which features the point falls in — area point-in-polygon, line/point within +//! a small radius — reporting each hit feature's S-57 class + attribute JSON + +//! source cell through a C callback. The engine hands the class/s57_json/cell on +//! the FeatureMeta contract, so no S-57 decode is needed here. +const std = @import("std"); +const rs = @import("surface.zig"); + +/// C callback: one call per feature the query point falls in. Pointers are valid +/// only for the duration of the call. +pub const QueryCb = extern struct { + ctx: ?*anyopaque, + feature: *const fn (?*anyopaque, cls: [*]const u8, cls_len: usize, s57: [*]const u8, s57_len: usize, cell: [*]const u8, cell_len: usize) callconv(.c) void, +}; + +pub const QuerySurface = struct { + qx: f64, + qy: f64, + radius: f64, // near-hit radius for line/point features (tile units) + cb: *const QueryCb, + cur: rs.FeatureMeta = .{}, + hit: bool = false, + + const vtable = rs.Surface.VTable{ + .beginScene = beginScene, + .beginFeature = beginFeature, + .fillArea = fillArea, + .fillPattern = fillPattern, + .strokeLine = strokeLine, + .drawSymbol = drawSymbol, + .drawSounding = drawSounding, + .drawText = drawText, + .endFeature = endFeature, + .endScene = endScene, + }; + + pub fn asSurface(self: *QuerySurface) rs.Surface { + return .{ .ptr = self, .vtable = &vtable }; + } + fn sp(ctx: *anyopaque) *QuerySurface { + return @ptrCast(@alignCast(ctx)); + } + + fn beginScene(_: *anyopaque, _: u8) anyerror!void {} + fn beginFeature(ctx: *anyopaque, meta: *const rs.FeatureMeta) anyerror!void { + const self = sp(ctx); + self.cur = meta.*; + self.hit = false; + } + fn endFeature(ctx: *anyopaque) anyerror!void { + const self = sp(ctx); + if (!self.hit) return; + const m = self.cur; + self.cb.feature(self.cb.ctx, m.class.ptr, m.class.len, m.s57_json.ptr, m.s57_json.len, m.cell_name.ptr, m.cell_name.len); + } + fn endScene(_: *anyopaque, out: std.mem.Allocator) anyerror![]u8 { + return out.alloc(u8, 0); + } + + // ---- point tests (tile-local coordinates) ------------------------------ + fn pointInRings(self: *QuerySurface, rings: []const []const rs.TilePoint) bool { + // Even-odd across every ring (exterior + holes): a point in a hole + // toggles twice and is correctly excluded. + var inside = false; + for (rings) |ring| { + if (ring.len < 3) continue; + var j: usize = ring.len - 1; + var i: usize = 0; + while (i < ring.len) : (i += 1) { + const xi: f64 = @floatFromInt(ring[i].x); + const yi: f64 = @floatFromInt(ring[i].y); + const xj: f64 = @floatFromInt(ring[j].x); + const yj: f64 = @floatFromInt(ring[j].y); + if ((yi > self.qy) != (yj > self.qy)) { + const xint = (xj - xi) * (self.qy - yi) / (yj - yi) + xi; + if (self.qx < xint) inside = !inside; + } + j = i; + } + } + return inside; + } + fn distSeg(self: *QuerySurface, a: rs.TilePoint, b: rs.TilePoint) f64 { + const ax: f64 = @floatFromInt(a.x); + const ay: f64 = @floatFromInt(a.y); + const dx: f64 = @as(f64, @floatFromInt(b.x)) - ax; + const dy: f64 = @as(f64, @floatFromInt(b.y)) - ay; + const len2 = dx * dx + dy * dy; + var t: f64 = 0; + if (len2 > 1e-9) t = std.math.clamp(((self.qx - ax) * dx + (self.qy - ay) * dy) / len2, 0, 1); + const ex = self.qx - (ax + t * dx); + const ey = self.qy - (ay + t * dy); + return @sqrt(ex * ex + ey * ey); + } + fn nearLines(self: *QuerySurface, lines: []const []const rs.TilePoint) bool { + for (lines) |line| { + var k: usize = 0; + while (k + 1 < line.len) : (k += 1) + if (self.distSeg(line[k], line[k + 1]) <= self.radius) return true; + } + return false; + } + fn nearPoint(self: *QuerySurface, at: rs.TilePoint) bool { + const dx = @as(f64, @floatFromInt(at.x)) - self.qx; + const dy = @as(f64, @floatFromInt(at.y)) - self.qy; + return dx * dx + dy * dy <= self.radius * self.radius; + } + + fn fillArea(ctx: *anyopaque, _: rs.ColorToken, rings: []const []const rs.TilePoint, _: ?rs.DepthRange) anyerror!void { + const self = sp(ctx); + if (self.pointInRings(rings)) self.hit = true; + } + fn fillPattern(ctx: *anyopaque, _: rs.SymbolName, rings: []const []const rs.TilePoint) anyerror!void { + const self = sp(ctx); + if (self.pointInRings(rings)) self.hit = true; + } + fn strokeLine(ctx: *anyopaque, _: rs.ColorToken, _: f64, _: rs.Dash, lines: []const []const rs.TilePoint, _: ?f64) anyerror!void { + const self = sp(ctx); + if (self.nearLines(lines)) self.hit = true; + } + fn drawSymbol(ctx: *anyopaque, _: rs.SymbolName, at: rs.TilePoint, _: f64, _: f64, _: bool, _: rs.SymbolPlacement, _: ?f64) anyerror!void { + const self = sp(ctx); + if (self.nearPoint(at)) self.hit = true; + } + fn drawSounding(ctx: *anyopaque, _: f64, _: bool, _: bool, at: rs.TilePoint) anyerror!void { + const self = sp(ctx); + if (self.nearPoint(at)) self.hit = true; + } + fn drawText(ctx: *anyopaque, _: []const u8, _: *const rs.TextStyle, at: rs.TilePoint) anyerror!void { + const self = sp(ctx); + if (self.nearPoint(at)) self.hit = true; + } +}; diff --git a/src/render/render.zig b/src/render/render.zig index 7d5236c..1d62f53 100644 --- a/src/render/render.zig +++ b/src/render/render.zig @@ -23,6 +23,7 @@ pub const surface = @import("surface.zig"); pub const noop = @import("noop.zig"); pub const inspect = @import("inspect.zig"); // recording surface (the `tile57 explore` learning tool) +pub const query = @import("query.zig"); // point-query surface (cursor object-query / pick) pub const resolve = @import("resolve.zig"); pub const canvas = @import("canvas.zig"); pub const raster = @import("raster.zig"); diff --git a/src/scene/scene.zig b/src/scene/scene.zig index 6a1861a..851b9a7 100644 --- a/src/scene/scene.zig +++ b/src/scene/scene.zig @@ -3567,6 +3567,8 @@ fn metaFromProps(props: []const mvt.Prop) rs.FeatureMeta { .vg = propInt(props, "vg", 0), .scamin = if (propOf(props, "scamin")) |_| propInt(props, "scamin", 0) else null, .class = propStr(props, "class"), + .s57_json = propStr(props, "s57"), // cursor-pick attribute blob (baked) + .cell_name = propStr(props, "cell"), // source cell badge (baked) .band = @intCast(std.math.clamp(propInt(props, "band", 0), 0, 255)), .bnd = propInt(props, "bnd", 2), .pts = propInt(props, "pts", 2), From ff9fe3bfec8b9ada701bbddf83c02c01ee1781ed Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 14:07:07 -0400 Subject: [PATCH 09/19] docs: document the host-surface, object-query, and sprite-mln C APIs c-api.md gains three sections in plain language: "Render to a host surface" (tile57_chart_render_surface_cb + the tile57_surface_cb vtable, including the draw_sprite/draw_pattern atlas callbacks), "Query the features under a point" (tile57_chart_query + tile57_query_cb, the S-52 cursor pick), and a note on tile57_bake_sprite_mln in the portrayal-assets section. CHANGELOG records all three plus the reader-path s57/cell FeatureMeta fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 17 +++++++++ docs/docs/c-api.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33eb1fe..7753891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ C ABI is not yet frozen. ## [Unreleased] +### Added — C ABI: host-surface rendering, object query, sprite atlas +- **`tile57_chart_render_surface_cb`** + the **`tile57_surface_cb`** vtable: hand + the portrayed scene to a host as world-space draw calls (area/line geometry in + web-mercator, point symbols/text as a world anchor plus a reference-px outline, + each call tagged with its SCAMIN) so a GPU host tessellates once and pans/zooms + by transforming the vertices. Includes optional `draw_sprite` / `draw_pattern` + callbacks that name atlas entries to draw point symbols and area patterns as + textured quads. See [docs/c-api.md](docs/docs/c-api.md). +- **`tile57_chart_query`** + **`tile57_query_cb`**: the S-52 cursor pick — report + every feature under a lon/lat (area point-in-polygon; line/point within a small + radius) with its class, S-57 attribute JSON, and source cell. +- **`tile57_bake_sprite_mln`**: bake just the MapLibre sprite-mln atlas (pivot- + centered symbol cells + a name-to-rect index) for a GPU host to load. +- **Fix**: the reader (baked PMTiles) replay now copies the baked `s57` / `cell` + tile properties into `FeatureMeta`, so a baked chart's object query returns + attributes, not just the class. + ### Added — S-52 overscale indication (AP(OVERSC01), specs/overscale.md) - **Baked overscale hatch geometry**: every cell contributing to a tile (including band-handoff carried cells) now emits its M_COVR (CATCOV=1) diff --git a/docs/docs/c-api.md b/docs/docs/c-api.md index e39d29e..462b45e 100644 --- a/docs/docs/c-api.md +++ b/docs/docs/c-api.md @@ -130,6 +130,60 @@ int tile57_chart_render_pdf(tile57_chart *chart, double lon, double lat, double The `tile57_mariner` settings struct is defined in the [chart-style section](#build-a-maplibre-style) below. +## Render to a host surface (vector callbacks) + +Instead of a finished raster, tile57 can hand you the portrayed scene as a stream +of draw calls in world space. A GPU host tessellates that stream once, then pans +and zooms by transforming the vertices each frame, so symbols and text stay a +constant size on screen and no re-portrayal is needed while the view moves. + +You fill in a `tile57_surface_cb` vtable and pass it to +`tile57_chart_render_surface_cb`. Area and line geometry come in web-mercator world +coordinates (the range 0 to 1, with y pointing down). Point symbols, soundings, and +text come as a world anchor plus a small outline in reference pixels, so you can +draw them at a fixed size on screen. Every call carries the feature's SCAMIN, so you +can hide it by zoom in a shader. + +```c +typedef struct { double x, y; } tile57_world_point; /* web-mercator 0..1, y down */ +typedef struct { const tile57_world_point *pts; uint32_t n; + const uint32_t *ring_starts; uint32_t ring_count; } tile57_world_rings; +typedef struct { const char *cls; int64_t scamin; int32_t plane; } tile57_feature; + +typedef struct { + void *ctx; /* handed back to every call */ + void (*fill_area) (void *ctx, const tile57_feature *f, const tile57_world_rings *rings, + tile57_rgba color, int even_odd); + void (*stroke_line)(void *ctx, const tile57_feature *f, const tile57_world_rings *lines, + float width_px, float dash_on, float dash_off, tile57_rgba color); + void (*draw_symbol)(void *ctx, const tile57_feature *f, tile57_world_point anchor, + const tile57_local_rings *rings, tile57_rgba color, int even_odd, float stroke_w); + void (*draw_text) (void *ctx, const tile57_feature *f, tile57_world_point anchor, + const tile57_local_rings *glyphs, tile57_rgba color, tile57_rgba halo, float halo_px); + /* Optional. Leave NULL to get vector outlines from the two calls above; set them + * to draw point symbols and area patterns from the sprite atlas as textured quads. */ + void (*draw_sprite) (void *ctx, const tile57_feature *f, const char *name, size_t name_len, + tile57_world_point anchor, float rot_deg, float half_w_px, float half_h_px); + void (*draw_pattern)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, + const tile57_world_rings *rings); +} tile57_surface_cb; + +/* Portray the view once and drive the callbacks. 0 ok, -1 bad handle, + * -2 render failure, -3 unsupported source. */ +int tile57_chart_render_surface_cb(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, const tile57_surface_cb *surface); +``` + +Set `draw_sprite` and `draw_pattern` once you have the sprite atlas loaded (see +[`tile57_bake_sprite_mln`](#generate-portrayal-assets)). tile57 then hands point +symbols, soundings, and area patterns by name, and you draw them as atlas quads — +smoothed by texture filtering and cheaper than tessellating outlines. If you leave +those two fields NULL, the same features arrive as vector outlines instead. + +tile57 also declutters overlapping text for you before it makes the calls (symbols +and soundings always draw, per S-52), so you don't repeat that work. + ## Inspect a chart: cells, features, catalogues ```c @@ -157,6 +211,33 @@ int tile57_catalog_entries(const uint8_t *catalog_031, size_t len, The CLI mirrors these as `tile57 cells`, `tile57 features`, and `tile57 catalog`. +## Query the features under a point (object query / pick) + +The S-52 cursor pick. Given a lon/lat, tile57 replays the finest tile that covers +the point and reports every feature the point falls in — an area you are inside, +or a line or point symbol within a small radius. Each hit calls you back with the +S-57 object-class acronym, the attribute JSON (acronym to value), and the source +cell name. This is what a chart application shows when you tap a feature to see +what it is. + +```c +typedef struct { + void *ctx; + void (*feature)(void *ctx, const char *cls, size_t cls_len, + const char *s57, size_t s57_len, + const char *cell, size_t cell_len); +} tile57_query_cb; + +/* Calls cb->feature once per feature under (lon,lat). 0 ok, -1 bad args. + * The pointers passed to the callback are valid only during that call. */ +int tile57_chart_query(tile57_chart *chart, double lon, double lat, const tile57_query_cb *cb); +``` + +The class and cell come through for any chart. The attribute JSON is filled in +only when the chart was baked with pick attributes — `tile57_bake_bundle` / +`tile57_bake_pmtiles` include them by default (set `omit_pick_attrs` to leave them +out for leaner tiles). Without them, `s57` is an empty string. + ## Bake an ENC_ROOT to PMTiles Bake in-memory cells into one PMTiles archive, zoom-banded per cell by @@ -237,6 +318,18 @@ int tile57_bake_assets(const char *catalog_dir, tile57_assets *out); /* 1 = o void tile57_assets_free(tile57_assets *out); ``` +`tile57_bake_sprite_mln` is a focused variant that fills only the `sprite_json` / +`sprite_png` fields with a MapLibre **sprite-mln** atlas: every S-101 symbol packed +into one PNG, each cell centered on its symbol's pivot, plus a JSON index of +`{name: {x, y, width, height, pixelRatio}}`. A GPU host loads this atlas once and +draws point symbols and area patterns as textured quads by name — the atlas the +[host-surface `draw_sprite`/`draw_pattern` callbacks](#render-to-a-host-surface-vector-callbacks) +hand back. Free it with `tile57_assets_free` as above. + +```c +int tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out); /* 1 = ok, 0 = error */ +``` + ## Build a MapLibre style `tile57_build_style` turns a MapLibre style template + the mariner's S-52 display From 1bc424c1b5c4a74ec4eb3a3ba32ccde6c40f9731 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 14:28:51 -0400 Subject: [PATCH 10/19] render: honor S-101 ColorFill transparency (fill areas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The S-101 ColorFill instruction is "NAME[,transparency]" — e.g. CHGRF,0.5 (50% see-through built-up area), TRFCF,0.75 (traffic), NODTA,0.5 (no data). The parser kept the whole "CHGRF,0.5" as the colour token, so it failed to resolve (rendered as the magenta FALLBACK) AND dropped the transparency. Add surface.fillToken to split "NAME[,transparency]" and map the transparency to alpha = (1 - transparency) * 255. Both the vector surface (GPU host) and the pixel surface (raster reference) now resolve the colour and apply the alpha, so those fills draw semi-transparent instead of opaque magenta. Line/text tokens have no comma and pass through unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/render/pixel.zig | 6 +++++- src/render/surface.zig | 12 ++++++++++++ src/render/vector.zig | 6 +++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/render/pixel.zig b/src/render/pixel.zig index 59e48f7..e21bec4 100644 --- a/src/render/pixel.zig +++ b/src/render/pixel.zig @@ -209,7 +209,11 @@ pub const PixelSurface = struct { _ = depth; // the rule resolved the depth token against the real context const self = sp(ctx); if (!self.cur_visible) return; - try self.push(.area, .{ .fill = .{ .rings = try self.toCanvas(rings), .color = self.resolveColor(token) } }); + // ColorFill "NAME[,transparency]": apply the S-101 fill transparency (alpha). + const ft = rs.fillToken(token); + var col = self.resolveColor(ft.name); + col.a = ft.alpha; + try self.push(.area, .{ .fill = .{ .rings = try self.toCanvas(rings), .color = col } }); } /// Device px per CSS px: the supersample factor (a 512px tile is the @2x diff --git a/src/render/surface.zig b/src/render/surface.zig index c307500..8a1a4c9 100644 --- a/src/render/surface.zig +++ b/src/render/surface.zig @@ -43,6 +43,18 @@ pub const SymbolPlacement = enum { point, line }; /// Null when the area is not a depth area. pub const DepthRange = struct { d1: f32, d2: f32 }; +/// Split an S-101 ColorFill token "NAME[,transparency]" into the colour name and +/// an alpha byte. `transparency` is the fraction transparent (0 = opaque .. 1 = +/// clear; e.g. `CHGRF,0.5` is 50% see-through), so alpha = (1 - transparency)*255. +/// No comma => fully opaque. Only area fills carry transparency (line/text tokens +/// have no comma, so they pass through unchanged). +pub fn fillToken(token: ColorToken) struct { name: []const u8, alpha: u8 } { + const comma = std.mem.indexOfScalar(u8, token, ',') orelse return .{ .name = token, .alpha = 255 }; + const t = std.fmt.parseFloat(f64, std.mem.trim(u8, token[comma + 1 ..], " ")) catch 0; + const a: u8 = @intFromFloat(std.math.clamp((1.0 - t) * 255.0, 0.0, 255.0)); + return .{ .name = token[0..comma], .alpha = a }; +} + /// Text-label style carried by drawText. /// /// An empty `halign` marks a MINIMAL label: no alignment/offset/halo/group was diff --git a/src/render/vector.zig b/src/render/vector.zig index 14c0b2b..8fdb4b2 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -276,7 +276,11 @@ pub const VectorSurface = struct { if (!self.cur_visible) return; const feat = self.cur_feature(); var wr = try self.worldRings(rings); - self.cb.fill_area(self.cb.ctx, &feat, &wr, ccolor(self.resolveColor(token)), 0); + // ColorFill "NAME[,transparency]": apply the S-101 fill transparency (alpha). + const ft = rs.fillToken(token); + var col = self.resolveColor(ft.name); + col.a = ft.alpha; + self.cb.fill_area(self.cb.ctx, &feat, &wr, ccolor(col), 0); } fn fillPattern(ctx: *anyopaque, name: rs.SymbolName, rings: []const []const rs.TilePoint) anyerror!void { From 18a285d86125cd5f2d07212020b81123594da112 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 14:44:32 -0400 Subject: [PATCH 11/19] render: object query at the view zoom, with the SCAMIN cull MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The query always used the finest tile, so it (1) reported features that SCAMIN hides at the view zoom and (2) used a pick radius that, in ground terms, was tiny when zoomed out — buoys and tracks were nearly impossible to tap. It also ignored SCAMIN entirely (tiles carry every feature; the display culls in-shader), so a zoomed-out click returned hundreds of finer-scale features. tile57_chart_query now takes the view zoom: it replays the tile at that zoom (pick tolerance tracks on-screen distance) and the QuerySurface applies resolve.scaminVisible against the view zoom, so it reports what's actually drawn. Same point at z12 went from 42 hits to 6; z16 stays an accurate 3. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/docs/c-api.md | 25 ++++++++++++++++--------- include/tile57.h | 7 +++++-- src/capi.zig | 10 +++++----- src/chart.zig | 10 ++++++++-- src/render/query.zig | 8 +++++++- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/docs/docs/c-api.md b/docs/docs/c-api.md index 462b45e..949191b 100644 --- a/docs/docs/c-api.md +++ b/docs/docs/c-api.md @@ -213,12 +213,18 @@ The CLI mirrors these as `tile57 cells`, `tile57 features`, and ## Query the features under a point (object query / pick) -The S-52 cursor pick. Given a lon/lat, tile57 replays the finest tile that covers -the point and reports every feature the point falls in — an area you are inside, -or a line or point symbol within a small radius. Each hit calls you back with the -S-57 object-class acronym, the attribute JSON (acronym to value), and the source -cell name. This is what a chart application shows when you tap a feature to see -what it is. +The S-52 cursor pick. Given a lon/lat and the current view `zoom`, tile57 replays +the tile at that zoom and reports every feature the point falls in — an area you +are inside, or a line or point symbol within a small radius. Each hit calls you +back with the S-57 object-class acronym, the attribute JSON (acronym to value), +and the source cell name. This is what a chart application shows when you tap a +feature to see what it is. + +Passing the view zoom matters: the query reports the features actually DISPLAYED +at that zoom (it applies the same SCAMIN cull the renderer does), and the pick +tolerance tracks on-screen distance instead of ground distance — so a buoy is just +as easy to tap zoomed out as zoomed in, and a zoomed-out click doesn't return +finer-scale features that aren't drawn. ```c typedef struct { @@ -228,9 +234,10 @@ typedef struct { const char *cell, size_t cell_len); } tile57_query_cb; -/* Calls cb->feature once per feature under (lon,lat). 0 ok, -1 bad args. - * The pointers passed to the callback are valid only during that call. */ -int tile57_chart_query(tile57_chart *chart, double lon, double lat, const tile57_query_cb *cb); +/* Calls cb->feature once per displayed feature under (lon,lat) at view `zoom`. + * 0 ok, -1 bad args. Callback pointers are valid only during that call. */ +int tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom, + const tile57_query_cb *cb); ``` The class and cell come through for any chart. The attribute JSON is filled in diff --git a/include/tile57.h b/include/tile57.h index 86bc37f..096c1b5 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -373,8 +373,11 @@ typedef struct { const char *s57, size_t s57_len, const char *cell, size_t cell_len); } tile57_query_cb; -/* Returns 0 ok, -1 bad args. */ -int tile57_chart_query(tile57_chart *chart, double lon, double lat, const tile57_query_cb *cb); +/* `zoom` is the current view's web-mercator zoom: the query uses the tile at that + * zoom, so it reports the features actually DISPLAYED there (SCAMIN-bucketed) and + * the pick tolerance tracks on-screen distance. Returns 0 ok, -1 bad args. */ +int tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom, + const tile57_query_cb *cb); /* The chart's per-cell metadata as a JSON array, one object per cell: * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", diff --git a/src/capi.zig b/src/capi.zig index 93c6a35..0eddb2c 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -150,13 +150,13 @@ export fn tile57_chart_get_info(src: ?*Chart, out: *CChartInfo) callconv(.c) voi const CQueryCb = @import("render").query.QueryCb; -/// Cursor object-query at (lon,lat): invokes cb->feature once per feature the -/// point falls in, with its S-57 class, attribute JSON, and source cell. -/// 0=ok, -1=bad args. See tile57.h. -export fn tile57_chart_query(handle: ?*Chart, lon: f64, lat: f64, cb: ?*const CQueryCb) callconv(.c) c_int { +/// Cursor object-query at (lon,lat) for the view `zoom` (web-mercator): invokes +/// cb->feature once per displayed feature the point falls in, with its S-57 class, +/// attribute JSON, and source cell. 0=ok, -1=bad args. See tile57.h. +export fn tile57_chart_query(handle: ?*Chart, lon: f64, lat: f64, zoom: f64, cb: ?*const CQueryCb) callconv(.c) c_int { const self = handle orelse return -1; const cbp = cb orelse return -1; - self.queryPoint(lon, lat, cbp) catch return -1; + self.queryPoint(lon, lat, zoom, cbp) catch return -1; return 0; } diff --git a/src/chart.zig b/src/chart.zig index 3f5811a..e3fd521 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -1015,12 +1015,17 @@ pub const Chart = struct { /// Cursor object-query: replay the finest tile covering (lon,lat) through a /// QuerySurface and report each feature the point falls in (class + S-57 /// attribute JSON + source cell) via `cb`. Used for the S-52 §10.8 pick. - pub fn queryPoint(self: *Chart, lon: f64, lat: f64, cb: *const render.query.QueryCb) !void { + pub fn queryPoint(self: *Chart, lon: f64, lat: f64, zoom: f64, cb: *const render.query.QueryCb) !void { const t = @import("tiles").tile; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); - const z: u8 = @intCast(self.zoomRange().max); + // Query the tile at the VIEW zoom, not the finest: its features are already + // SCAMIN-bucketed to what's displayed, the tile exists (it's what's drawn), + // and the pick radius (tile units) maps to a constant on-screen distance. + const zr = self.zoomRange(); + const zc = std.math.clamp(@round(zoom), @as(f64, @floatFromInt(zr.min)), @as(f64, @floatFromInt(zr.max))); + const z: u8 = @intFromFloat(zc); const world = t.lonLatToWorld(lon, lat); const n = std.math.exp2(@as(f64, @floatFromInt(z))); const tx: u32 = @intFromFloat(@floor(world[0] * n)); @@ -1030,6 +1035,7 @@ pub const Chart = struct { .qx = @floatFromInt(local.x), .qy = @floatFromInt(local.y), .radius = 96.0, // ~6 px at native tile scale + .view_zoom = zoom, // raw view zoom for the SCAMIN cull .cb = cb, }; const surf = qs.asSurface(); diff --git a/src/render/query.zig b/src/render/query.zig index 8c03136..2a8c248 100644 --- a/src/render/query.zig +++ b/src/render/query.zig @@ -6,6 +6,7 @@ //! the FeatureMeta contract, so no S-57 decode is needed here. const std = @import("std"); const rs = @import("surface.zig"); +const resolve = @import("resolve.zig"); /// C callback: one call per feature the query point falls in. Pointers are valid /// only for the duration of the call. @@ -18,9 +19,11 @@ pub const QuerySurface = struct { qx: f64, qy: f64, radius: f64, // near-hit radius for line/point features (tile units) + view_zoom: f64, // the view zoom, for the SCAMIN visibility cull cb: *const QueryCb, cur: rs.FeatureMeta = .{}, hit: bool = false, + visible: bool = false, // current feature passes SCAMIN at view_zoom const vtable = rs.Surface.VTable{ .beginScene = beginScene, @@ -47,10 +50,13 @@ pub const QuerySurface = struct { const self = sp(ctx); self.cur = meta.*; self.hit = false; + // Only report what the view actually shows: apply the same SCAMIN cull the + // renderer does, so a zoomed-out click doesn't return finer-scale features. + self.visible = resolve.scaminVisible(meta.scamin, self.view_zoom); } fn endFeature(ctx: *anyopaque) anyerror!void { const self = sp(ctx); - if (!self.hit) return; + if (!self.hit or !self.visible) return; const m = self.cur; self.cb.feature(self.cb.ctx, m.class.ptr, m.class.len, m.s57_json.ptr, m.s57_json.len, m.cell_name.ptr, m.cell_name.len); } From 47a8f6c9acf8418b52545a3fc65435fd758905a6 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 15:55:50 -0400 Subject: [PATCH 12/19] sprite: SDF glyph atlas via stb_truetype (GPU text, phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendor stb_truetype.h (public domain, same source as the existing stb_image_write) and add a tg_glyph_sdf wrapper in svgraster.c over stbtt_GetCodepointSDF + metrics. src/sprite/glyph.zig builds an SDF atlas for a codepoint set: one signed-distance field per glyph, shelf-packed into an RGBA texture, with per-glyph EM-space metrics (offset/size/advance + UV). This is the standard MapLibre/game text pipeline and replaces per-frame glyph-outline tessellation (the profiled tess_local_even_odd / 182k-vert text hotspot) with 6-verts-per-glyph textured quads that stay crisp at any size — the reason we need SDF (multiple font sizes from one atlas). Lives in the sprite module (shares its stb/nanosvg C glue + libc) as a baked asset, like the sprite atlas. Tested: valid field (edge + inside values), glyph metrics, and PNG export. Next: bake export + wire drawText/plugin to draw the quads. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.zig | 7 + src/sprite/glyph.zig | 180 ++ src/sprite/sprite.zig | 4 + src/sprite/svgraster.c | 26 + vendor/stb/stb_truetype.h | 5079 +++++++++++++++++++++++++++++++++++++ 5 files changed, 5296 insertions(+) create mode 100644 src/sprite/glyph.zig create mode 100644 vendor/stb/stb_truetype.h diff --git a/build.zig b/build.zig index 220d4ec..1a4670d 100644 --- a/build.zig +++ b/build.zig @@ -527,6 +527,13 @@ pub fn build(b: *std.Build) void { // module import does NOT pull another module's `test {}` blocks in. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = mod })).step); + // The sprite module (SDF glyph atlas lives here): needs the C glue + // (stb_truetype/nanosvg) + libc + render. + const sprite_test = addPkgTest(b, test_step, "src/sprite/sprite.zig", target, optimize, &.{ + .{ .name = "render", .module = render_mod }, + }); + sprite_test.link_libc = true; + addSvgRaster(b, sprite_test); _ = addPkgTest(b, test_step, "src/s57/s57.zig", target, optimize, &.{}); const s100_test = addPkgTest(b, test_step, "src/s100/s100.zig", target, optimize, &.{ diff --git a/src/sprite/glyph.zig b/src/sprite/glyph.zig new file mode 100644 index 0000000..c4bb3c2 --- /dev/null +++ b/src/sprite/glyph.zig @@ -0,0 +1,180 @@ +//! SDF glyph atlas: rasterize the label font's glyphs as signed-distance fields +//! (via vendored stb_truetype) into one texture, so a GPU host draws text as +//! textured quads that stay crisp at any size — the MapLibre / game text path. +//! Lives in the sprite module because it shares that module's C glue (stb) and, +//! like the sprite atlas, is a baked asset the host loads once. +const std = @import("std"); +const Allocator = std.mem.Allocator; + +// C glue (src/sprite/svgraster.c): stb_truetype SDF + the shared PNG encoder. +extern fn tg_glyph_sdf(font: [*]const u8, font_len: c_int, cp: c_int, em_px: f32, pad: c_int, w: *c_int, h: *c_int, xoff: *c_int, yoff: *c_int, advance: *f32) callconv(.c) ?[*]u8; +extern fn tg_glyph_free(p: ?[*]u8) callconv(.c) void; +extern fn tg_png_encode(rgba: [*]const u8, w: c_int, h: c_int, out_len: *c_int) callconv(.c) ?[*]u8; +extern fn tg_svg_free(p: ?*anyopaque) callconv(.c) void; + +/// Per-glyph placement in EM units relative to the pen origin (x = pen, +/// y = baseline, y DOWN), plus its atlas UV rect (normalized 0..1). +pub const GlyphInfo = struct { + u0: f32 = 0, + v0: f32 = 0, + u1: f32 = 0, + v1: f32 = 0, + off_x: f32 = 0, + off_y: f32 = 0, + w: f32 = 0, + h: f32 = 0, + advance: f32 = 0, +}; + +pub const Atlas = struct { + rgba: []u8, // width*height*4, SDF replicated into RGBA + width: u32, + height: u32, + em_px: f32, // px one em was rasterized at + pad: f32, // SDF spread in px (shader edge reference = pad/em_px in EM) + glyphs: std.AutoHashMapUnmanaged(u21, GlyphInfo) = .{}, + + pub fn deinit(self: *Atlas, a: Allocator) void { + a.free(self.rgba); + self.glyphs.deinit(a); + } + pub fn info(self: *const Atlas, cp: u21) ?GlyphInfo { + return self.glyphs.get(cp); + } + /// Encode the atlas as PNG (caller owns; free with std allocator). Uses the + /// shared C encoder. + pub fn encodePng(self: *const Atlas, a: Allocator) !?[]u8 { + var len: c_int = 0; + const p = tg_png_encode(self.rgba.ptr, @intCast(self.width), @intCast(self.height), &len) orelse return null; + defer tg_svg_free(p); + return try a.dupe(u8, p[0..@intCast(len)]); + } +}; + +/// Printable ASCII + Latin-1 supplement — English labels plus common accented +/// place-names. +pub fn defaultCodepoints(a: Allocator) ![]u21 { + var list = std.ArrayList(u21).empty; + var c: u21 = 0x20; + while (c <= 0x7E) : (c += 1) try list.append(a, c); + c = 0xA0; + while (c <= 0xFF) : (c += 1) try list.append(a, c); + return list.toOwnedSlice(a); +} + +const Cell = struct { cp: u21, w: u32, h: u32, sdf: ?[*]u8, gi: GlyphInfo }; + +/// Build an SDF atlas for `cps`. `em_px` sets the field resolution (px per em), +/// `pad` the spread in px. +pub fn build(a: Allocator, font: []const u8, cps: []const u21, em_px: f32, pad: u32) !Atlas { + const atlas_w: u32 = 512; + + var cells = std.ArrayList(Cell).empty; + defer { + for (cells.items) |c| if (c.sdf) |p| tg_glyph_free(p); + cells.deinit(a); + } + + for (cps) |cp| { + var w: c_int = 0; + var h: c_int = 0; + var xoff: c_int = 0; + var yoff: c_int = 0; + var adv: f32 = 0; + const sdf = tg_glyph_sdf(font.ptr, @intCast(font.len), @intCast(cp), em_px, @intCast(pad), &w, &h, &xoff, &yoff, &adv); + const gi = GlyphInfo{ + .off_x = @as(f32, @floatFromInt(xoff)) / em_px, + .off_y = @as(f32, @floatFromInt(yoff)) / em_px, + .w = @as(f32, @floatFromInt(w)) / em_px, + .h = @as(f32, @floatFromInt(h)) / em_px, + .advance = adv / em_px, + }; + try cells.append(a, .{ .cp = cp, .w = @intCast(@max(w, 0)), .h = @intCast(@max(h, 0)), .sdf = sdf, .gi = gi }); + } + + // Shelf-pack: rows wrap at atlas_w; compute total height. + const gp: u32 = 1; + var cx: u32 = gp; + var cy: u32 = gp; + var row_h: u32 = 0; + var total_h: u32 = gp; + for (cells.items) |c| { + if (c.sdf == null or c.w == 0) continue; + if (cx + c.w + gp > atlas_w) { + cy += row_h + gp; + cx = gp; + row_h = 0; + } + cx += c.w + gp; + row_h = @max(row_h, c.h); + total_h = cy + row_h + gp; + } + const atlas_h = total_h; + + const rgba = try a.alloc(u8, atlas_w * atlas_h * 4); + @memset(rgba, 0); + var glyphs = std.AutoHashMapUnmanaged(u21, GlyphInfo){}; + + cx = gp; + cy = gp; + row_h = 0; + for (cells.items) |c| { + var gi = c.gi; + if (c.sdf) |sdf| if (c.w != 0) { + if (cx + c.w + gp > atlas_w) { + cy += row_h + gp; + cx = gp; + row_h = 0; + } + var yy: u32 = 0; + while (yy < c.h) : (yy += 1) { + var xx: u32 = 0; + while (xx < c.w) : (xx += 1) { + const v = sdf[yy * c.w + xx]; + const o = ((cy + yy) * atlas_w + (cx + xx)) * 4; + rgba[o] = v; + rgba[o + 1] = v; + rgba[o + 2] = v; + rgba[o + 3] = v; + } + } + const fw: f32 = @floatFromInt(atlas_w); + const fh: f32 = @floatFromInt(atlas_h); + gi.u0 = @as(f32, @floatFromInt(cx)) / fw; + gi.v0 = @as(f32, @floatFromInt(cy)) / fh; + gi.u1 = @as(f32, @floatFromInt(cx + c.w)) / fw; + gi.v1 = @as(f32, @floatFromInt(cy + c.h)) / fh; + cx += c.w + gp; + row_h = @max(row_h, c.h); + }; + try glyphs.put(a, c.cp, gi); + } + + return .{ .rgba = rgba, .width = atlas_w, .height = atlas_h, .em_px = em_px, .pad = @floatFromInt(pad), .glyphs = glyphs }; +} + +test "glyph atlas: SDF cells + metrics for ASCII" { + const a = std.testing.allocator; + const font = @import("render").font.notosans; + const cps = try defaultCodepoints(a); + defer a.free(cps); + var atlas = try build(a, font, cps, 32.0, 6); + defer atlas.deinit(a); + + try std.testing.expect(atlas.width == 512 and atlas.height > 0); + const gA = atlas.info('A').?; + try std.testing.expect(gA.advance > 0 and gA.w > 0 and gA.u1 > gA.u0); + // edge values present (a valid field, not all-inside/outside) + var hi = false; + var mid = false; + for (atlas.rgba) |v| { + if (v > 220) hi = true; + if (v > 40 and v < 210) mid = true; + } + try std.testing.expect(hi and mid); + + // The host loads a PNG of the atlas — verify the encoder produces one. + const png = (try atlas.encodePng(a)).?; + defer a.free(png); + try std.testing.expect(png.len > 8 and png[0] == 0x89 and png[1] == 'P' and png[2] == 'N'); +} diff --git a/src/sprite/sprite.zig b/src/sprite/sprite.zig index c4acfef..6d17f5b 100644 --- a/src/sprite/sprite.zig +++ b/src/sprite/sprite.zig @@ -16,6 +16,10 @@ extern fn tg_svg_rasterize(svg: [*:0]u8, scale: f32, out_w: *c_int, out_h: *c_in extern fn tg_png_encode(rgba: [*]const u8, w: c_int, h: c_int, out_len: *c_int) ?[*]u8; extern fn tg_svg_free(p: ?*anyopaque) void; +/// SDF glyph atlas for GPU text (stb_truetype), a sibling baked asset. +pub const glyph = @import("glyph.zig"); +test { _ = glyph; } + /// device px per 0.01-mm symbol unit (matches the Go oracle's raster.go pxPerUnit). pub const px_per_unit: f64 = 0.08; const px_per_mm: f64 = px_per_unit * 100.0; // 8 px/mm diff --git a/src/sprite/svgraster.c b/src/sprite/svgraster.c index 4e71808..984981f 100644 --- a/src/sprite/svgraster.c +++ b/src/sprite/svgraster.c @@ -18,6 +18,32 @@ #include "nanosvgrast.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" +#define STB_TRUETYPE_IMPLEMENTATION +#include "stb_truetype.h" + +// One glyph's signed-distance field for the SDF text atlas. `font`/`font_len` is +// the TrueType file; `cp` a codepoint; `em_px` the pixels one em maps to (the +// field resolution); `pad` the SDF spread in px. Returns a malloc'd single-channel +// SDF bitmap (w*h, 128 = edge, >128 inside) and sets its size, the top-left pixel +// offset from the pen origin (y down), and the pen advance — all in `em_px` units, +// so the caller normalizes by em_px to get size-independent (em) metrics. NULL for +// blank glyphs (space): *w=*h=0 but *advance is still set. Free with tg_glyph_free. +unsigned char *tg_glyph_sdf(const unsigned char *font, int font_len, int cp, + float em_px, int pad, int *w, int *h, + int *xoff, int *yoff, float *advance) { + (void)font_len; + stbtt_fontinfo f; + if (!stbtt_InitFont(&f, font, stbtt_GetFontOffsetForIndex(font, 0))) return NULL; + float scale = stbtt_ScaleForMappingEmToPixels(&f, em_px); + int adv = 0, lsb = 0; + stbtt_GetCodepointHMetrics(&f, cp, &adv, &lsb); + *advance = (float)adv * scale; + unsigned char onedge = 128; + float dist_scale = 127.0f / (float)pad; // ±pad px -> the byte range + *w = 0; *h = 0; *xoff = 0; *yoff = 0; + return stbtt_GetCodepointSDF(&f, scale, cp, pad, onedge, dist_scale, w, h, xoff, yoff); +} +void tg_glyph_free(unsigned char *p) { stbtt_FreeSDF(p, NULL); } // Rasterize a flattened SVG (viewBox normalized to "0 0 W H") at `scale` device // px per user unit. Forces even-odd winding on every shape — the S-101 danger diff --git a/vendor/stb/stb_truetype.h b/vendor/stb/stb_truetype.h new file mode 100644 index 0000000..90a5c2e --- /dev/null +++ b/vendor/stb/stb_truetype.h @@ -0,0 +1,5079 @@ +// stb_truetype.h - v1.26 - public domain +// authored from 2009-2021 by Sean Barrett / RAD Game Tools +// +// ======================================================================= +// +// NO SECURITY GUARANTEE -- DO NOT USE THIS ON UNTRUSTED FONT FILES +// +// This library does no range checking of the offsets found in the file, +// meaning an attacker can use it to read arbitrary memory. +// +// ======================================================================= +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// render glyphs to one-channel SDF bitmaps (signed-distance field/function) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// Dougall Johnson: OpenType / Type 2 font handling +// Daniel Ribeiro Maciel: basic GPOS-based kerning +// +// Misc other: +// Ryan Gordon +// Simon Glass +// github:IntellectualKitty +// Imanol Celaya +// Daniel Ribeiro Maciel +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket Fabian "ryg" Giesen github:NiLuJe +// Cass Everitt Martins Mozeiko github:aloucks +// stoiko (Haemimont Games) Cap Petschulat github:oyvindjam +// Brian Hook Omar Cornut github:vassvik +// Walter van Niftrik Ryan Griege +// David Gow Peter LaValle +// David Given Sergey Popov +// Ivan-Assen Ivanov Giumo X. Clanjor +// Anthony Pesch Higor Euripedes +// Johan Duparc Thomas Fields +// Hou Qiming Derek Vinyard +// Rob Loach Cort Stratton +// Kenney Phillis Jr. Brian Costabile +// Ken Voskuil (kaesve) Yakov Galka +// +// VERSION HISTORY +// +// 1.26 (2021-08-28) fix broken rasterizer +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) GPOS kerning, STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// See end of file for license information. +// +// USAGE +// +// Include this file in whatever places need to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversampling() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- indexing for TTC font collections +// stbtt_GetNumberOfFonts() -- number of fonts for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetFontVMetricsOS2() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// DETAILED USAGE: +// +// Scale: +// Select how high you want the font to be, in points or pixels. +// Call ScaleForPixelHeight or ScaleForMappingEmToPixels to compute +// a scale factor SF that will be used by all other functions. +// +// Baseline: +// You need to select a y-coordinate that is the baseline of where +// your text will appear. Call GetFontBoundingBox to get the baseline-relative +// bounding box for all characters. SF*-y0 will be the distance in pixels +// that the worst-case character could extend above the baseline, so if +// you want the top edge of characters to appear at the top of the +// screen where y=0, then you would set the baseline to SF*-y0. +// +// Current point: +// Set the current point where the first character will appear. The +// first character could extend left of the current point; this is font +// dependent. You can either choose a current point that is the leftmost +// point and hope, or add some padding, or check the bounding box or +// left-side-bearing of the first character to be displayed and set +// the current point based on that. +// +// Displaying a character: +// Compute the bounding box of the character. It will contain signed values +// relative to . I.e. if it returns x0,y0,x1,y1, +// then the character should be displayed in the rectangle from +// to = 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype, e.g. if you don't +//// link with the C runtime library. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // e.g. #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + #ifndef STBTT_sqrt + #include + #define STBTT_sqrt(x) sqrt(x) + #define STBTT_pow(x,y) pow(x,y) + #endif + + #ifndef STBTT_fmod + #include + #define STBTT_fmod(x,y) fmod(x,y) + #endif + + #ifndef STBTT_cos + #include + #define STBTT_cos(x) cos(x) + #define STBTT_acos(x) acos(x) + #endif + + #ifndef STBTT_fabs + #include + #define STBTT_fabs(x) fabs(x) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) ((void)(u),malloc(x)) + #define STBTT_free(x,u) ((void)(u),free(x)) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else +#define STBTT_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// private structure +typedef struct +{ + unsigned char *data; + int cursor; + int size; +} stbtt__buf; + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap); +// Query the font vertical metrics without having to create a font first. + + +////////////////////////////////////////////////////////////////////////////// +// +// NEW TEXTURE BAKING API +// +// This provides options for packing multiple fonts into one atlas, not +// perfectly but better than nothing. + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; + float xoff2,yoff2; +} stbtt_packedchar; + +typedef struct stbtt_pack_context stbtt_pack_context; +typedef struct stbtt_fontinfo stbtt_fontinfo; +#ifndef STB_RECT_PACK_VERSION +typedef struct stbrp_rect stbrp_rect; +#endif + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); +// Initializes a packing context stored in the passed-in stbtt_pack_context. +// Future calls using this context will pack characters into the bitmap passed +// in here: a 1-channel bitmap that is width * height. stride_in_bytes is +// the distance from one row to the next (or 0 to mean they are packed tightly +// together). "padding" is the amount of padding to leave between each +// character (normally you want '1' for bitmaps you'll use as textures with +// bilinear filtering). +// +// Returns 0 on failure, 1 on success. + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); +// Cleans up the packing context and frees all memory. + +#define STBTT_POINT_SIZE(x) (-(x)) + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); +// Creates character bitmaps from the font_index'th font found in fontdata (use +// font_index=0 if you don't know what that is). It creates num_chars_in_range +// bitmaps for characters with unicode values starting at first_unicode_char_in_range +// and increasing. Data for how to render them is stored in chardata_for_range; +// pass these to stbtt_GetPackedQuad to get back renderable quads. +// +// font_size is the full height of the character from ascender to descender, +// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed +// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() +// and pass that result as 'font_size': +// ..., 20 , ... // font max minus min y is 20 pixels tall +// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + +typedef struct +{ + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally +} stbtt_pack_range; + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); +// Creates character bitmaps from multiple ranges of characters stored in +// ranges. This will usually create a better-packed bitmap than multiple +// calls to stbtt_PackFontRange. Note that you can call this multiple +// times within a single PackBegin/PackEnd. + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); +// Oversampling a font increases the quality by allowing higher-quality subpixel +// positioning, and is especially valuable at smaller text sizes. +// +// This function sets the amount of oversampling for all following calls to +// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given +// pack context. The default (no oversampling) is achieved by h_oversample=1 +// and v_oversample=1. The total number of pixels required is +// h_oversample*v_oversample larger than the default; for example, 2x2 +// oversampling requires 4x the storage of 1x1. For best results, render +// oversampled textures with bilinear filtering. Look at the readme in +// stb/tests/oversample for information about oversampled fonts +// +// To use with PackFontRangesGather etc., you must set it before calls +// call to PackFontRangesGatherRects. + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip); +// If skip != 0, this tells stb_truetype to skip any codepoints for which +// there is no corresponding glyph. If skip=0, which is the default, then +// codepoints without a glyph recived the font's "missing character" glyph, +// typically an empty box by convention. + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +// Calling these functions in sequence is roughly equivalent to calling +// stbtt_PackFontRanges(). If you more control over the packing of multiple +// fonts, or if you want to pack custom data into a font texture, take a look +// at the source to of stbtt_PackFontRanges() and create a custom version +// using these functions, e.g. call GatherRects multiple times, +// building up a single array of rects, then call PackRects once, +// then call RenderIntoRects repeatedly. This may result in a +// better packing than calling PackFontRanges multiple times +// (or it may not). + +// this is an opaque structure that you shouldn't mess with which holds +// all the context needed from PackBegin to PackEnd. +struct stbtt_pack_context { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + int skip_missing; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data); +// This function will determine the number of fonts in a font file. TrueType +// collection (.ttc) files may contain multiple fonts, while TrueType font +// (.ttf) files only contain one font. The number of fonts can be used for +// indexing with the previous function where the index is between zero and one +// less than the total fonts. If an error occurs, -1 is returned. + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. + +// The following structure is defined publicly so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern,gpos,svg; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph + + stbtt__buf cff; // cff font data + stbtt__buf charstrings; // the charstring index + stbtt__buf gsubrs; // global charstring subroutines index + stbtt__buf subrs; // private charstring subroutines index + stbtt__buf fontdicts; // array of font dicts + stbtt__buf fdselect; // map from glyph to fontdict +}; + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. +// Returns 0 if the character codepoint is not defined in the font. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap); +// analogous to GetFontVMetrics, but returns the "typographic" values from the OS/2 +// table (specific to MS/Windows TTF files). +// +// Returns 1 on success (table present), 0 on failure. + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + +typedef struct stbtt_kerningentry +{ + int glyph1; // use stbtt_FindGlyphIndex + int glyph2; + int advance; +} stbtt_kerningentry; + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info); +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length); +// Retrieves a complete list of all of the kerning pairs provided by the font +// stbtt_GetKerningTable never writes more than table_length entries and returns how many entries it did write. +// The table will be sorted by (a.glyph1 == b.glyph1)?(a.glyph2 < b.glyph2):(a.glyph1 < b.glyph1) + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve, + STBTT_vcubic + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy,cx1,cy1; + unsigned char type,padding; + } stbtt_vertex; +#endif + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates +// +// The shape is a series of contours. Each one starts with +// a STBTT_moveto, then consists of a series of mixed +// STBTT_lineto and STBTT_curveto segments. A lineto +// draws a line from previous endpoint to its x,y; a curveto +// draws a quadratic bezier from previous endpoint to +// its x,y, using cx,cy as the bezier control point. + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +STBTT_DEF unsigned char *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl); +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg); +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg); +// fills svg with the character's SVG data. +// returns data size or 0 if SVG not found. + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint); +// same as stbtt_MakeCodepointBitmapSubpixel, but prefiltering +// is performed (see stbtt_PackSetOversampling) + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int glyph); +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +// rasterize a shape with quadratic beziers into a bitmap +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + +////////////////////////////////////////////////////////////////////////////// +// +// Signed Distance Function (or Field) rendering + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata); +// frees the SDF bitmap allocated below + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +// These functions compute a discretized SDF field for a single character, suitable for storing +// in a single-channel texture, sampling with bilinear filtering, and testing against +// larger than some threshold to produce scalable fonts. +// info -- the font +// scale -- controls the size of the resulting SDF bitmap, same as it would be creating a regular bitmap +// glyph/codepoint -- the character to generate the SDF for +// padding -- extra "pixels" around the character which are filled with the distance to the character (not 0), +// which allows effects like bit outlines +// onedge_value -- value 0-255 to test the SDF against to reconstruct the character (i.e. the isocontour of the character) +// pixel_dist_scale -- what value the SDF should increase by when moving one SDF "pixel" away from the edge (on the 0..255 scale) +// if positive, > onedge_value is inside; if negative, < onedge_value is inside +// width,height -- output height & width of the SDF bitmap (including padding) +// xoff,yoff -- output origin of the character +// return value -- a 2D array of bytes 0..255, width*height in size +// +// pixel_dist_scale & onedge_value are a scale & bias that allows you to make +// optimal use of the limited 0..255 for your application, trading off precision +// and special effects. SDF values outside the range 0..255 are clamped to 0..255. +// +// Example: +// scale = stbtt_ScaleForPixelHeight(22) +// padding = 5 +// onedge_value = 180 +// pixel_dist_scale = 180/5.0 = 36.0 +// +// This will create an SDF bitmap in which the character is about 22 pixels +// high but the whole bitmap is about 22+5+5=32 pixels high. To produce a filled +// shape, sample the SDF at each pixel and fill the pixel if the SDF value +// is greater than or equal to 180/255. (You'll actually want to antialias, +// which is beyond the scope of this example.) Additionally, you can compute +// offset outlines (e.g. to stroke the character border inside & outside, +// or only outside). For example, to fill outside the character up to 3 SDF +// pixels, you would compare against (180-36.0*3)/255 = 72/255. The above +// choice of variables maps a range from 5 pixels outside the shape to +// 2 pixels inside the shape to 0..255; this is intended primarily for apply +// outside effects only (the interior range is needed to allow proper +// antialiasing of the font at *smaller* sizes) +// +// The function computes the SDF analytically at each SDF pixel, not by e.g. +// building a higher-res bitmap and approximating it. In theory the quality +// should be as high as possible for an SDF of this size & representation, but +// unclear if this is true in practice (perhaps building a higher-res bitmap +// and computing from that can allow drop-out prevention). +// +// The algorithm has not been optimized at all, so expect it to be slow +// if computing lots of characters or very large sizes. + + + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#endif + +#if STBTT_MAX_OVERSAMPLE > 255 +#error "STBTT_MAX_OVERSAMPLE cannot be > 255" +#endif + +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +#ifdef _MSC_VER +#define STBTT__NOTUSED(v) (void)(v) +#else +#define STBTT__NOTUSED(v) (void)sizeof(v) +#endif + +////////////////////////////////////////////////////////////////////////// +// +// stbtt__buf helpers to parse data from file +// + +static stbtt_uint8 stbtt__buf_get8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor++]; +} + +static stbtt_uint8 stbtt__buf_peek8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor]; +} + +static void stbtt__buf_seek(stbtt__buf *b, int o) +{ + STBTT_assert(!(o > b->size || o < 0)); + b->cursor = (o > b->size || o < 0) ? b->size : o; +} + +static void stbtt__buf_skip(stbtt__buf *b, int o) +{ + stbtt__buf_seek(b, b->cursor + o); +} + +static stbtt_uint32 stbtt__buf_get(stbtt__buf *b, int n) +{ + stbtt_uint32 v = 0; + int i; + STBTT_assert(n >= 1 && n <= 4); + for (i = 0; i < n; i++) + v = (v << 8) | stbtt__buf_get8(b); + return v; +} + +static stbtt__buf stbtt__new_buf(const void *p, size_t size) +{ + stbtt__buf r; + STBTT_assert(size < 0x40000000); + r.data = (stbtt_uint8*) p; + r.size = (int) size; + r.cursor = 0; + return r; +} + +#define stbtt__buf_get16(b) stbtt__buf_get((b), 2) +#define stbtt__buf_get32(b) stbtt__buf_get((b), 4) + +static stbtt__buf stbtt__buf_range(const stbtt__buf *b, int o, int s) +{ + stbtt__buf r = stbtt__new_buf(NULL, 0); + if (o < 0 || s < 0 || o > b->size || s > b->size - o) return r; + r.data = b->data + o; + r.size = s; + return r; +} + +static stbtt__buf stbtt__cff_get_index(stbtt__buf *b) +{ + int count, start, offsize; + start = b->cursor; + count = stbtt__buf_get16(b); + if (count) { + offsize = stbtt__buf_get8(b); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(b, offsize * count); + stbtt__buf_skip(b, stbtt__buf_get(b, offsize) - 1); + } + return stbtt__buf_range(b, start, b->cursor - start); +} + +static stbtt_uint32 stbtt__cff_int(stbtt__buf *b) +{ + int b0 = stbtt__buf_get8(b); + if (b0 >= 32 && b0 <= 246) return b0 - 139; + else if (b0 >= 247 && b0 <= 250) return (b0 - 247)*256 + stbtt__buf_get8(b) + 108; + else if (b0 >= 251 && b0 <= 254) return -(b0 - 251)*256 - stbtt__buf_get8(b) - 108; + else if (b0 == 28) return stbtt__buf_get16(b); + else if (b0 == 29) return stbtt__buf_get32(b); + STBTT_assert(0); + return 0; +} + +static void stbtt__cff_skip_operand(stbtt__buf *b) { + int v, b0 = stbtt__buf_peek8(b); + STBTT_assert(b0 >= 28); + if (b0 == 30) { + stbtt__buf_skip(b, 1); + while (b->cursor < b->size) { + v = stbtt__buf_get8(b); + if ((v & 0xF) == 0xF || (v >> 4) == 0xF) + break; + } + } else { + stbtt__cff_int(b); + } +} + +static stbtt__buf stbtt__dict_get(stbtt__buf *b, int key) +{ + stbtt__buf_seek(b, 0); + while (b->cursor < b->size) { + int start = b->cursor, end, op; + while (stbtt__buf_peek8(b) >= 28) + stbtt__cff_skip_operand(b); + end = b->cursor; + op = stbtt__buf_get8(b); + if (op == 12) op = stbtt__buf_get8(b) | 0x100; + if (op == key) return stbtt__buf_range(b, start, end-start); + } + return stbtt__buf_range(b, 0, 0); +} + +static void stbtt__dict_get_ints(stbtt__buf *b, int key, int outcount, stbtt_uint32 *out) +{ + int i; + stbtt__buf operands = stbtt__dict_get(b, key); + for (i = 0; i < outcount && operands.cursor < operands.size; i++) + out[i] = stbtt__cff_int(&operands); +} + +static int stbtt__cff_index_count(stbtt__buf *b) +{ + stbtt__buf_seek(b, 0); + return stbtt__buf_get16(b); +} + +static stbtt__buf stbtt__cff_index_get(stbtt__buf b, int i) +{ + int count, offsize, start, end; + stbtt__buf_seek(&b, 0); + count = stbtt__buf_get16(&b); + offsize = stbtt__buf_get8(&b); + STBTT_assert(i >= 0 && i < count); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(&b, i*offsize); + start = stbtt__buf_get(&b, offsize); + end = stbtt__buf_get(&b, offsize); + return stbtt__buf_range(&b, 2+(count+1)*offsize+start, end - start); +} + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +static stbtt_uint16 ttUSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_int16 ttSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_uint32 ttULONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } +static stbtt_int32 ttLONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + if (stbtt_tag(font, "true")) return 1; // Apple specification for TrueType fonts + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +static int stbtt_GetFontOffsetForIndex_internal(unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*4); + } + } + return -1; +} + +static int stbtt_GetNumberOfFonts_internal(unsigned char *font_collection) +{ + // if it's just a font, there's only one valid font + if (stbtt__isfont(font_collection)) + return 1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + return ttLONG(font_collection+8); + } + } + return 0; +} + +static stbtt__buf stbtt__get_subrs(stbtt__buf cff, stbtt__buf fontdict) +{ + stbtt_uint32 subrsoff = 0, private_loc[2] = { 0, 0 }; + stbtt__buf pdict; + stbtt__dict_get_ints(&fontdict, 18, 2, private_loc); + if (!private_loc[1] || !private_loc[0]) return stbtt__new_buf(NULL, 0); + pdict = stbtt__buf_range(&cff, private_loc[1], private_loc[0]); + stbtt__dict_get_ints(&pdict, 19, 1, &subrsoff); + if (!subrsoff) return stbtt__new_buf(NULL, 0); + stbtt__buf_seek(&cff, private_loc[1]+subrsoff); + return stbtt__cff_get_index(&cff); +} + +// since most people won't use this, find this table the first time it's needed +static int stbtt__get_svg(stbtt_fontinfo *info) +{ + stbtt_uint32 t; + if (info->svg < 0) { + t = stbtt__find_table(info->data, info->fontstart, "SVG "); + if (t) { + stbtt_uint32 offset = ttULONG(info->data + t + 2); + info->svg = t + offset; + } else { + info->svg = 0; + } + } + return info->svg; +} + +static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, int fontstart) +{ + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + info->cff = stbtt__new_buf(NULL, 0); + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + info->gpos = stbtt__find_table(data, fontstart, "GPOS"); // not required + + if (!cmap || !info->head || !info->hhea || !info->hmtx) + return 0; + if (info->glyf) { + // required for truetype + if (!info->loca) return 0; + } else { + // initialization for CFF / Type2 fonts (OTF) + stbtt__buf b, topdict, topdictidx; + stbtt_uint32 cstype = 2, charstrings = 0, fdarrayoff = 0, fdselectoff = 0; + stbtt_uint32 cff; + + cff = stbtt__find_table(data, fontstart, "CFF "); + if (!cff) return 0; + + info->fontdicts = stbtt__new_buf(NULL, 0); + info->fdselect = stbtt__new_buf(NULL, 0); + + // @TODO this should use size from table (not 512MB) + info->cff = stbtt__new_buf(data+cff, 512*1024*1024); + b = info->cff; + + // read the header + stbtt__buf_skip(&b, 2); + stbtt__buf_seek(&b, stbtt__buf_get8(&b)); // hdrsize + + // @TODO the name INDEX could list multiple fonts, + // but we just use the first one. + stbtt__cff_get_index(&b); // name INDEX + topdictidx = stbtt__cff_get_index(&b); + topdict = stbtt__cff_index_get(topdictidx, 0); + stbtt__cff_get_index(&b); // string INDEX + info->gsubrs = stbtt__cff_get_index(&b); + + stbtt__dict_get_ints(&topdict, 17, 1, &charstrings); + stbtt__dict_get_ints(&topdict, 0x100 | 6, 1, &cstype); + stbtt__dict_get_ints(&topdict, 0x100 | 36, 1, &fdarrayoff); + stbtt__dict_get_ints(&topdict, 0x100 | 37, 1, &fdselectoff); + info->subrs = stbtt__get_subrs(b, topdict); + + // we only support Type 2 charstrings + if (cstype != 2) return 0; + if (charstrings == 0) return 0; + + if (fdarrayoff) { + // looks like a CID font + if (!fdselectoff) return 0; + stbtt__buf_seek(&b, fdarrayoff); + info->fontdicts = stbtt__cff_get_index(&b); + info->fdselect = stbtt__buf_range(&b, fdselectoff, b.size-fdselectoff); + } + + stbtt__buf_seek(&b, charstrings); + info->charstrings = stbtt__cff_get_index(&b); + } + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + info->svg = -1; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start, last; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + last = ttUSHORT(data + endCount + 2*item); + if (unicode_codepoint < start || unicode_codepoint > last) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + STBTT_assert(!info->cff.size); + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); + +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + if (info->cff.size) { + stbtt__GetGlyphInfoT2(info, glyph_index, x0, y0, x1, y1); + } else { + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + } + return 1; +} + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g; + if (info->cff.size) + return stbtt__GetGlyphInfoT2(info, glyph_index, NULL, NULL, NULL, NULL) == 0; + g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours < 0) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0 && vertices) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); + STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +typedef struct +{ + int bounds; + int started; + float first_x, first_y; + float x, y; + stbtt_int32 min_x, max_x, min_y, max_y; + + stbtt_vertex *pvertices; + int num_vertices; +} stbtt__csctx; + +#define STBTT__CSCTX_INIT(bounds) {bounds,0, 0,0, 0,0, 0,0,0,0, NULL, 0} + +static void stbtt__track_vertex(stbtt__csctx *c, stbtt_int32 x, stbtt_int32 y) +{ + if (x > c->max_x || !c->started) c->max_x = x; + if (y > c->max_y || !c->started) c->max_y = y; + if (x < c->min_x || !c->started) c->min_x = x; + if (y < c->min_y || !c->started) c->min_y = y; + c->started = 1; +} + +static void stbtt__csctx_v(stbtt__csctx *c, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy, stbtt_int32 cx1, stbtt_int32 cy1) +{ + if (c->bounds) { + stbtt__track_vertex(c, x, y); + if (type == STBTT_vcubic) { + stbtt__track_vertex(c, cx, cy); + stbtt__track_vertex(c, cx1, cy1); + } + } else { + stbtt_setvertex(&c->pvertices[c->num_vertices], type, x, y, cx, cy); + c->pvertices[c->num_vertices].cx1 = (stbtt_int16) cx1; + c->pvertices[c->num_vertices].cy1 = (stbtt_int16) cy1; + } + c->num_vertices++; +} + +static void stbtt__csctx_close_shape(stbtt__csctx *ctx) +{ + if (ctx->first_x != ctx->x || ctx->first_y != ctx->y) + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->first_x, (int)ctx->first_y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rmove_to(stbtt__csctx *ctx, float dx, float dy) +{ + stbtt__csctx_close_shape(ctx); + ctx->first_x = ctx->x = ctx->x + dx; + ctx->first_y = ctx->y = ctx->y + dy; + stbtt__csctx_v(ctx, STBTT_vmove, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rline_to(stbtt__csctx *ctx, float dx, float dy) +{ + ctx->x += dx; + ctx->y += dy; + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rccurve_to(stbtt__csctx *ctx, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) +{ + float cx1 = ctx->x + dx1; + float cy1 = ctx->y + dy1; + float cx2 = cx1 + dx2; + float cy2 = cy1 + dy2; + ctx->x = cx2 + dx3; + ctx->y = cy2 + dy3; + stbtt__csctx_v(ctx, STBTT_vcubic, (int)ctx->x, (int)ctx->y, (int)cx1, (int)cy1, (int)cx2, (int)cy2); +} + +static stbtt__buf stbtt__get_subr(stbtt__buf idx, int n) +{ + int count = stbtt__cff_index_count(&idx); + int bias = 107; + if (count >= 33900) + bias = 32768; + else if (count >= 1240) + bias = 1131; + n += bias; + if (n < 0 || n >= count) + return stbtt__new_buf(NULL, 0); + return stbtt__cff_index_get(idx, n); +} + +static stbtt__buf stbtt__cid_get_glyph_subrs(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt__buf fdselect = info->fdselect; + int nranges, start, end, v, fmt, fdselector = -1, i; + + stbtt__buf_seek(&fdselect, 0); + fmt = stbtt__buf_get8(&fdselect); + if (fmt == 0) { + // untested + stbtt__buf_skip(&fdselect, glyph_index); + fdselector = stbtt__buf_get8(&fdselect); + } else if (fmt == 3) { + nranges = stbtt__buf_get16(&fdselect); + start = stbtt__buf_get16(&fdselect); + for (i = 0; i < nranges; i++) { + v = stbtt__buf_get8(&fdselect); + end = stbtt__buf_get16(&fdselect); + if (glyph_index >= start && glyph_index < end) { + fdselector = v; + break; + } + start = end; + } + } + if (fdselector == -1) stbtt__new_buf(NULL, 0); + return stbtt__get_subrs(info->cff, stbtt__cff_index_get(info->fontdicts, fdselector)); +} + +static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, stbtt__csctx *c) +{ + int in_header = 1, maskbits = 0, subr_stack_height = 0, sp = 0, v, i, b0; + int has_subrs = 0, clear_stack; + float s[48]; + stbtt__buf subr_stack[10], subrs = info->subrs, b; + float f; + +#define STBTT__CSERR(s) (0) + + // this currently ignores the initial width value, which isn't needed if we have hmtx + b = stbtt__cff_index_get(info->charstrings, glyph_index); + while (b.cursor < b.size) { + i = 0; + clear_stack = 1; + b0 = stbtt__buf_get8(&b); + switch (b0) { + // @TODO implement hinting + case 0x13: // hintmask + case 0x14: // cntrmask + if (in_header) + maskbits += (sp / 2); // implicit "vstem" + in_header = 0; + stbtt__buf_skip(&b, (maskbits + 7) / 8); + break; + + case 0x01: // hstem + case 0x03: // vstem + case 0x12: // hstemhm + case 0x17: // vstemhm + maskbits += (sp / 2); + break; + + case 0x15: // rmoveto + in_header = 0; + if (sp < 2) return STBTT__CSERR("rmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-2], s[sp-1]); + break; + case 0x04: // vmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("vmoveto stack"); + stbtt__csctx_rmove_to(c, 0, s[sp-1]); + break; + case 0x16: // hmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("hmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-1], 0); + break; + + case 0x05: // rlineto + if (sp < 2) return STBTT__CSERR("rlineto stack"); + for (; i + 1 < sp; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + // hlineto/vlineto and vhcurveto/hvcurveto alternate horizontal and vertical + // starting from a different place. + + case 0x07: // vlineto + if (sp < 1) return STBTT__CSERR("vlineto stack"); + goto vlineto; + case 0x06: // hlineto + if (sp < 1) return STBTT__CSERR("hlineto stack"); + for (;;) { + if (i >= sp) break; + stbtt__csctx_rline_to(c, s[i], 0); + i++; + vlineto: + if (i >= sp) break; + stbtt__csctx_rline_to(c, 0, s[i]); + i++; + } + break; + + case 0x1F: // hvcurveto + if (sp < 4) return STBTT__CSERR("hvcurveto stack"); + goto hvcurveto; + case 0x1E: // vhcurveto + if (sp < 4) return STBTT__CSERR("vhcurveto stack"); + for (;;) { + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], (sp - i == 5) ? s[i + 4] : 0.0f); + i += 4; + hvcurveto: + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], (sp - i == 5) ? s[i+4] : 0.0f, s[i+3]); + i += 4; + } + break; + + case 0x08: // rrcurveto + if (sp < 6) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x18: // rcurveline + if (sp < 8) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp - 2; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + if (i + 1 >= sp) return STBTT__CSERR("rcurveline stack"); + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + case 0x19: // rlinecurve + if (sp < 8) return STBTT__CSERR("rlinecurve stack"); + for (; i + 1 < sp - 6; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + if (i + 5 >= sp) return STBTT__CSERR("rlinecurve stack"); + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x1A: // vvcurveto + case 0x1B: // hhcurveto + if (sp < 4) return STBTT__CSERR("(vv|hh)curveto stack"); + f = 0.0; + if (sp & 1) { f = s[i]; i++; } + for (; i + 3 < sp; i += 4) { + if (b0 == 0x1B) + stbtt__csctx_rccurve_to(c, s[i], f, s[i+1], s[i+2], s[i+3], 0.0); + else + stbtt__csctx_rccurve_to(c, f, s[i], s[i+1], s[i+2], 0.0, s[i+3]); + f = 0.0; + } + break; + + case 0x0A: // callsubr + if (!has_subrs) { + if (info->fdselect.size) + subrs = stbtt__cid_get_glyph_subrs(info, glyph_index); + has_subrs = 1; + } + // FALLTHROUGH + case 0x1D: // callgsubr + if (sp < 1) return STBTT__CSERR("call(g|)subr stack"); + v = (int) s[--sp]; + if (subr_stack_height >= 10) return STBTT__CSERR("recursion limit"); + subr_stack[subr_stack_height++] = b; + b = stbtt__get_subr(b0 == 0x0A ? subrs : info->gsubrs, v); + if (b.size == 0) return STBTT__CSERR("subr not found"); + b.cursor = 0; + clear_stack = 0; + break; + + case 0x0B: // return + if (subr_stack_height <= 0) return STBTT__CSERR("return outside subr"); + b = subr_stack[--subr_stack_height]; + clear_stack = 0; + break; + + case 0x0E: // endchar + stbtt__csctx_close_shape(c); + return 1; + + case 0x0C: { // two-byte escape + float dx1, dx2, dx3, dx4, dx5, dx6, dy1, dy2, dy3, dy4, dy5, dy6; + float dx, dy; + int b1 = stbtt__buf_get8(&b); + switch (b1) { + // @TODO These "flex" implementations ignore the flex-depth and resolution, + // and always draw beziers. + case 0x22: // hflex + if (sp < 7) return STBTT__CSERR("hflex stack"); + dx1 = s[0]; + dx2 = s[1]; + dy2 = s[2]; + dx3 = s[3]; + dx4 = s[4]; + dx5 = s[5]; + dx6 = s[6]; + stbtt__csctx_rccurve_to(c, dx1, 0, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, -dy2, dx6, 0); + break; + + case 0x23: // flex + if (sp < 13) return STBTT__CSERR("flex stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = s[10]; + dy6 = s[11]; + //fd is s[12] + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + case 0x24: // hflex1 + if (sp < 9) return STBTT__CSERR("hflex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dx4 = s[5]; + dx5 = s[6]; + dy5 = s[7]; + dx6 = s[8]; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, dy5, dx6, -(dy1+dy2+dy5)); + break; + + case 0x25: // flex1 + if (sp < 11) return STBTT__CSERR("flex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = dy6 = s[10]; + dx = dx1+dx2+dx3+dx4+dx5; + dy = dy1+dy2+dy3+dy4+dy5; + if (STBTT_fabs(dx) > STBTT_fabs(dy)) + dy6 = -dy; + else + dx6 = -dx; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + default: + return STBTT__CSERR("unimplemented"); + } + } break; + + default: + if (b0 != 255 && b0 != 28 && b0 < 32) + return STBTT__CSERR("reserved operator"); + + // push immediate + if (b0 == 255) { + f = (float)(stbtt_int32)stbtt__buf_get32(&b) / 0x10000; + } else { + stbtt__buf_skip(&b, -1); + f = (float)(stbtt_int16)stbtt__cff_int(&b); + } + if (sp >= 48) return STBTT__CSERR("push stack overflow"); + s[sp++] = f; + clear_stack = 0; + break; + } + if (clear_stack) sp = 0; + } + return STBTT__CSERR("no endchar"); + +#undef STBTT__CSERR +} + +static int stbtt__GetGlyphShapeT2(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + // runs the charstring twice, once to count and once to output (to avoid realloc) + stbtt__csctx count_ctx = STBTT__CSCTX_INIT(1); + stbtt__csctx output_ctx = STBTT__CSCTX_INIT(0); + if (stbtt__run_charstring(info, glyph_index, &count_ctx)) { + *pvertices = (stbtt_vertex*)STBTT_malloc(count_ctx.num_vertices*sizeof(stbtt_vertex), info->userdata); + output_ctx.pvertices = *pvertices; + if (stbtt__run_charstring(info, glyph_index, &output_ctx)) { + STBTT_assert(output_ctx.num_vertices == count_ctx.num_vertices); + return output_ctx.num_vertices; + } + } + *pvertices = NULL; + return 0; +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + stbtt__csctx c = STBTT__CSCTX_INIT(1); + int r = stbtt__run_charstring(info, glyph_index, &c); + if (x0) *x0 = r ? c.min_x : 0; + if (y0) *y0 = r ? c.min_y : 0; + if (x1) *x1 = r ? c.max_x : 0; + if (y1) *y1 = r ? c.max_y : 0; + return r ? c.num_vertices : 0; +} + +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + if (!info->cff.size) + return stbtt__GetGlyphShapeTT(info, glyph_index, pvertices); + else + return stbtt__GetGlyphShapeT2(info, glyph_index, pvertices); +} + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info) +{ + stbtt_uint8 *data = info->data + info->kern; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + return ttUSHORT(data+10); +} + +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length) +{ + stbtt_uint8 *data = info->data + info->kern; + int k, length; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + length = ttUSHORT(data+10); + if (table_length < length) + length = table_length; + + for (k = 0; k < length; k++) + { + table[k].glyph1 = ttUSHORT(data+18+(k*6)); + table[k].glyph2 = ttUSHORT(data+20+(k*6)); + table[k].advance = ttSHORT(data+22+(k*6)); + } + + return length; +} + +static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) +{ + stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); + switch (coverageFormat) { + case 1: { + stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); + + // Binary search. + stbtt_int32 l=0, r=glyphCount-1, m; + int straw, needle=glyph; + while (l <= r) { + stbtt_uint8 *glyphArray = coverageTable + 4; + stbtt_uint16 glyphID; + m = (l + r) >> 1; + glyphID = ttUSHORT(glyphArray + 2 * m); + straw = glyphID; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + return m; + } + } + break; + } + + case 2: { + stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); + stbtt_uint8 *rangeArray = coverageTable + 4; + + // Binary search. + stbtt_int32 l=0, r=rangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *rangeRecord; + m = (l + r) >> 1; + rangeRecord = rangeArray + 6 * m; + strawStart = ttUSHORT(rangeRecord); + strawEnd = ttUSHORT(rangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else { + stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); + return startCoverageIndex + glyph - strawStart; + } + } + break; + } + + default: return -1; // unsupported + } + + return -1; +} + +static stbtt_int32 stbtt__GetGlyphClass(stbtt_uint8 *classDefTable, int glyph) +{ + stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); + switch (classDefFormat) + { + case 1: { + stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); + stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); + stbtt_uint8 *classDef1ValueArray = classDefTable + 6; + + if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) + return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + break; + } + + case 2: { + stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); + stbtt_uint8 *classRangeRecords = classDefTable + 4; + + // Binary search. + stbtt_int32 l=0, r=classRangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *classRangeRecord; + m = (l + r) >> 1; + classRangeRecord = classRangeRecords + 6 * m; + strawStart = ttUSHORT(classRangeRecord); + strawEnd = ttUSHORT(classRangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else + return (stbtt_int32)ttUSHORT(classRangeRecord + 4); + } + break; + } + + default: + return -1; // Unsupported definition type, return an error. + } + + // "All glyphs not assigned to a class fall into class 0". (OpenType spec) + return 0; +} + +// Define to STBTT_assert(x) if you want to break on unimplemented formats. +#define STBTT_GPOS_TODO_assert(x) + +static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint16 lookupListOffset; + stbtt_uint8 *lookupList; + stbtt_uint16 lookupCount; + stbtt_uint8 *data; + stbtt_int32 i, sti; + + if (!info->gpos) return 0; + + data = info->data + info->gpos; + + if (ttUSHORT(data+0) != 1) return 0; // Major version 1 + if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 + + lookupListOffset = ttUSHORT(data+8); + lookupList = data + lookupListOffset; + lookupCount = ttUSHORT(lookupList); + + for (i=0; i= pairSetCount) return 0; + + needle=glyph2; + r=pairValueCount-1; + l=0; + + // Binary search. + while (l <= r) { + stbtt_uint16 secondGlyph; + stbtt_uint8 *pairValue; + m = (l + r) >> 1; + pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; + secondGlyph = ttUSHORT(pairValue); + straw = secondGlyph; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + stbtt_int16 xAdvance = ttSHORT(pairValue + 2); + return xAdvance; + } + } + } else + return 0; + break; + } + + case 2: { + stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); + stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + if (valueFormat1 == 4 && valueFormat2 == 0) { // Support more formats? + stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); + stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); + int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); + int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); + + stbtt_uint16 class1Count = ttUSHORT(table + 12); + stbtt_uint16 class2Count = ttUSHORT(table + 14); + stbtt_uint8 *class1Records, *class2Records; + stbtt_int16 xAdvance; + + if (glyph1class < 0 || glyph1class >= class1Count) return 0; // malformed + if (glyph2class < 0 || glyph2class >= class2Count) return 0; // malformed + + class1Records = table + 16; + class2Records = class1Records + 2 * (glyph1class * class2Count); + xAdvance = ttSHORT(class2Records + 2 * glyph2class); + return xAdvance; + } else + return 0; + break; + } + + default: + return 0; // Unsupported position format + } + } + } + + return 0; +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int g2) +{ + int xAdvance = 0; + + if (info->gpos) + xAdvance += stbtt__GetGlyphGPOSInfoAdvance(info, g1, g2); + else if (info->kern) + xAdvance += stbtt__GetGlyphKernInfoAdvance(info, g1, g2); + + return xAdvance; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern && !info->gpos) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap) +{ + int tab = stbtt__find_table(info->data, info->fontstart, "OS/2"); + if (!tab) + return 0; + if (typoAscent ) *typoAscent = ttSHORT(info->data+tab + 68); + if (typoDescent) *typoDescent = ttSHORT(info->data+tab + 70); + if (typoLineGap) *typoLineGap = ttSHORT(info->data+tab + 72); + return 1; +} + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +STBTT_DEF stbtt_uint8 *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl) +{ + int i; + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc_list = data + stbtt__get_svg((stbtt_fontinfo *) info); + + int numEntries = ttUSHORT(svg_doc_list); + stbtt_uint8 *svg_docs = svg_doc_list + 2; + + for(i=0; i= ttUSHORT(svg_doc)) && (gl <= ttUSHORT(svg_doc + 2))) + return svg_doc; + } + return 0; +} + +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg) +{ + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc; + + if (info->svg == 0) + return 0; + + svg_doc = stbtt_FindSVGDoc(info, gl); + if (svg_doc != NULL) { + *svg = (char *) data + info->svg + ttULONG(svg_doc + 4); + return ttULONG(svg_doc + 8); + } else { + return 0; + } +} + +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg) +{ + return stbtt_GetGlyphSVG(info, stbtt_FindGlyphIndex(info, unicode_codepoint), svg); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { + // e.g. space character + if (ix0) *ix0 = 0; + if (iy0) *iy0 = 0; + if (ix1) *ix1 = 0; + if (iy1) *iy1 = 0; + } else { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); + if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); + if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); + } +} + +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + struct stbtt__hheap_chunk *next; +} stbtt__hheap_chunk; + +typedef struct stbtt__hheap +{ + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; +} stbtt__hheap; + +static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) +{ + if (hh->first_free) { + void *p = hh->first_free; + hh->first_free = * (void **) p; + return p; + } else { + if (hh->num_remaining_in_head_chunk == 0) { + int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + sizeof(stbtt__hheap_chunk) + size * hh->num_remaining_in_head_chunk; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *p) +{ + *(void **) p = hh->first_free; + hh->first_free = p; +} + +static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) +{ + stbtt__hheap_chunk *c = hh->head; + while (c) { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + + +typedef struct stbtt__active_edge +{ + struct stbtt__active_edge *next; + #if STBTT_RASTERIZER_VERSION==1 + int x,dx; + float ey; + int direction; + #elif STBTT_RASTERIZER_VERSION==2 + float fx,fdx,fdy; + float direction; + float sy; + float ey; + #else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" + #endif +} stbtt__active_edge; + +#if STBTT_RASTERIZER_VERSION == 1 +#define STBTT_FIXSHIFT 10 +#define STBTT_FIX (1 << STBTT_FIXSHIFT) +#define STBTT_FIXMASK (STBTT_FIX-1) + +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +#elif STBTT_RASTERIZER_VERSION == 2 +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + //STBTT_assert(e->y0 <= start_point); + if (!z) return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#if STBTT_RASTERIZER_VERSION == 1 +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->direction; + } else { + int x1 = e->x; w += e->direction; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +#elif STBTT_RASTERIZER_VERSION == 2 + +// the edge passed in here does not cross the vertical line at x or the vertical line at x+1 +// (i.e. it has already been clipped to those) +static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) +{ + if (y0 == y1) return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) return; + if (y1 < e->sy) return; + if (y0 < e->sy) { + x0 += (x1-x0) * (e->sy - y0) / (y1-y0); + y0 = e->sy; + } + if (y1 > e->ey) { + x1 += (x1-x0) * (e->ey - y1) / (y1-y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x+1); + else if (x0 == x+1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x+1) + STBTT_assert(x1 >= x+1); + else + STBTT_assert(x1 >= x && x1 <= x+1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1-y0); + else if (x0 >= x+1 && x1 >= x+1) + ; + else { + STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); + scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position + } +} + +static float stbtt__sized_trapezoid_area(float height, float top_width, float bottom_width) +{ + STBTT_assert(top_width >= 0); + STBTT_assert(bottom_width >= 0); + return (top_width + bottom_width) / 2.0f * height; +} + +static float stbtt__position_trapezoid_area(float height, float tx0, float tx1, float bx0, float bx1) +{ + return stbtt__sized_trapezoid_area(height, tx1 - tx0, bx1 - bx0); +} + +static float stbtt__sized_triangle_area(float height, float width) +{ + return height * width / 2; +} + +static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) +{ + float y_bottom = y_top+1; + + while (e) { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) { + float x0 = e->fx; + if (x0 < len) { + if (x0 >= 0) { + stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); + stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); + } else { + stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); + } + } + } else { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0,sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } else { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } else { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = (sy1 - sy0) * e->direction; + STBTT_assert(x >= 0 && x < len); + scanline[x] += stbtt__position_trapezoid_area(height, x_top, x+1.0f, x_bottom, x+1.0f); + scanline_fill[x] += height; // everything right of this pixel is filled + } else { + int x,x1,x2; + float y_crossing, y_final, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + } + STBTT_assert(dy >= 0); + STBTT_assert(dx >= 0); + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = y_top + dy * (x1+1 - x0); + + // compute intersection with y axis at x2 + y_final = y_top + dy * (x2 - x0); + + // x1 x_top x2 x_bottom + // y_top +------|-----+------------+------------+--------|---+------------+ + // | | | | | | + // | | | | | | + // sy0 | Txxxxx|............|............|............|............| + // y_crossing | *xxxxx.......|............|............|............| + // | | xxxxx..|............|............|............| + // | | /- xx*xxxx........|............|............| + // | | dy < | xxxxxx..|............|............| + // y_final | | \- | xx*xxx.........|............| + // sy1 | | | | xxxxxB...|............| + // | | | | | | + // | | | | | | + // y_bottom +------------+------------+------------+------------+------------+ + // + // goal is to measure the area covered by '.' in each pixel + + // if x2 is right at the right edge of x1, y_crossing can blow up, github #1057 + // @TODO: maybe test against sy1 rather than y_bottom? + if (y_crossing > y_bottom) + y_crossing = y_bottom; + + sign = e->direction; + + // area of the rectangle covered from sy0..y_crossing + area = sign * (y_crossing-sy0); + + // area of the triangle (x_top,sy0), (x1+1,sy0), (x1+1,y_crossing) + scanline[x1] += stbtt__sized_triangle_area(area, x1+1 - x_top); + + // check if final y_crossing is blown up; no test case for this + if (y_final > y_bottom) { + y_final = y_bottom; + dy = (y_final - y_crossing ) / (x2 - (x1+1)); // if denom=0, y_final = y_crossing, so y_final <= y_bottom + } + + // in second pixel, area covered by line segment found in first pixel + // is always a rectangle 1 wide * the height of that line segment; this + // is exactly what the variable 'area' stores. it also gets a contribution + // from the line segment within it. the THIRD pixel will get the first + // pixel's rectangle contribution, the second pixel's rectangle contribution, + // and its own contribution. the 'own contribution' is the same in every pixel except + // the leftmost and rightmost, a trapezoid that slides down in each pixel. + // the second pixel's contribution to the third pixel will be the + // rectangle 1 wide times the height change in the second pixel, which is dy. + + step = sign * dy * 1; // dy is dy/dx, change in y for every 1 change in x, + // which multiplied by 1-pixel-width is how much pixel area changes for each step in x + // so the area advances by 'step' every time + + for (x = x1+1; x < x2; ++x) { + scanline[x] += area + step/2; // area of trapezoid is 1*step/2 + area += step; + } + STBTT_assert(STBTT_fabs(area) <= 1.01f); // accumulated error from area += step unless we round step down + STBTT_assert(sy1 > y_final-0.01f); + + // area covered in the last pixel is the rectangle from all the pixels to the left, + // plus the trapezoid filled by the line segment in this pixel all the way to the right edge + scanline[x2] += area + sign * stbtt__position_trapezoid_area(sy1-y_final, (float) x2, x2+1.0f, x_bottom, x2+1.0f); + + // the rest of the line is filled based on the total height of the line segment in this pixel + scanline_fill[x2] += sign * (sy1-sy0); + } + } else { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + // note though that this does happen some of the time because + // x_top and x_bottom can be extrapolated at the top & bottom of + // the shape and actually lie outside the bounding box + int x; + for (x=0; x < len; ++x) { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clearly-defined pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x+1); + float x3 = xb; + float y3 = y_bottom; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + float y1 = (x - x0) / dx + y_top; + float y2 = (x+1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) { // three segments descending down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x1 && x0 > x2) { // three segments descending down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else { // one segment + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); + } + } + } + } + e = e->next; + } +} + +// directly AA rasterize edges w/o supersampling +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0, i; + float scanline_data[129], *scanline, *scanline2; + + STBTT__NOTUSED(vsubsample); + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y_top) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) { + if (e->y0 != e->y1) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) { + if (j == 0 && off_y != 0) { + if (z->ey < scan_y_top) { + // this can happen due to subpixel positioning and some kind of fp rounding error i think + z->ey = scan_y_top; + } + } + STBTT_assert(z->ey >= scan_y_top); // if we get really unlucky a tiny bit of an edge can be out of bounds + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i=0; i < result->w; ++i) { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k)*255 + 0.5f; + m = (int) k; + if (m > 255) m = 255; + result->pixels[j*result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + +static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) +{ + int i,j; + for (i=1; i < n; ++i) { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) { + stbtt__edge *b = &p[j-1]; + int c = STBTT__COMPARE(a,b); + if (!c) break; + p[j] = p[j-1]; + --j; + } + if (i != j) + p[j] = t; + } +} + +static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) +{ + /* threshold for transitioning to insertion sort */ + while (n > 12) { + stbtt__edge t; + int c01,c12,c,m,i,j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0],&p[m]); + c12 = STBTT__COMPARE(&p[m],&p[n-1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0],&p[n-1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n-1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i=1; + j=n-1; + for(;;) { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;;++i) { + if (!STBTT__COMPARE(&p[i], &p[0])) break; + } + for (;;--j) { + if (!STBTT__COMPARE(&p[0], &p[j])) break; + } + /* make sure we haven't crossed */ + if (i >= j) break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n-i)) { + stbtt__sort_edges_quicksort(p,j); + p = p+i; + n = n-i; + } else { + stbtt__sort_edges_quicksort(p+i, n-i); + n = j; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; +#if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +#elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +#else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +static void stbtt__tesselate_cubic(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float objspace_flatness_squared, int n) +{ + // @TODO this "flatness" calculation is just made-up nonsense that seems to work well enough + float dx0 = x1-x0; + float dy0 = y1-y0; + float dx1 = x2-x1; + float dy1 = y2-y1; + float dx2 = x3-x2; + float dy2 = y3-y2; + float dx = x3-x0; + float dy = y3-y0; + float longlen = (float) (STBTT_sqrt(dx0*dx0+dy0*dy0)+STBTT_sqrt(dx1*dx1+dy1*dy1)+STBTT_sqrt(dx2*dx2+dy2*dy2)); + float shortlen = (float) STBTT_sqrt(dx*dx+dy*dy); + float flatness_squared = longlen*longlen-shortlen*shortlen; + + if (n > 16) // 65536 segments on one curve better be enough! + return; + + if (flatness_squared > objspace_flatness_squared) { + float x01 = (x0+x1)/2; + float y01 = (y0+y1)/2; + float x12 = (x1+x2)/2; + float y12 = (y1+y2)/2; + float x23 = (x2+x3)/2; + float y23 = (y2+y3)/2; + + float xa = (x01+x12)/2; + float ya = (y01+y12)/2; + float xb = (x12+x23)/2; + float yb = (y12+y23)/2; + + float mx = (xa+xb)/2; + float my = (ya+yb)/2; + + stbtt__tesselate_cubic(points, num_points, x0,y0, x01,y01, xa,ya, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_cubic(points, num_points, mx,my, xb,yb, x23,y23, x3,y3, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x3,y3); + *num_points = *num_points+1; + } +} + +// returns number of contours +static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + case STBTT_vcubic: + stbtt__tesselate_cubic(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].cx1, vertices[i].cy1, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count = 0; + int *winding_lengths = NULL; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixelPrefilter(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, oversample_x, oversample_y, sub_x, sub_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +static int stbtt_BakeFontBitmap_internal(unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y+gh+1 > bottom_y) + bottom_y = y+gh+1; + } + return bottom_y; +} + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// rectangle packing replacement routines if you don't have stb_rect_pack.h +// + +#ifndef STB_RECT_PACK_VERSION + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + stbrp_coord x,y; + int id,w,h,was_packed; +}; + +static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) +{ + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); +} + +static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) +{ + int i; + for (i=0; i < num_rects; ++i) { + if (con->x + rects[i].w > con->width) { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for ( ; i < num_rects; ++i) + rects[i].was_packed = 0; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) +{ + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + + if (context == NULL || nodes == NULL) { + if (context != NULL) STBTT_free(context, alloc_context); + if (nodes != NULL) STBTT_free(nodes , alloc_context); + return 0; + } + + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; + spc->skip_missing = 0; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +{ + STBTT_free(spc->nodes , spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); +} + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) +{ + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; +} + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip) +{ + spc->skip_missing = skip; +} + +#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) + +static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < h; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } +} + +static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < w; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) { + STBTT_assert(pixels[i*stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + if (!oversample) + return 0.0f; + + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float)-(oversample - 1) / (2.0f * (float)oversample); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k; + int missing_glyph_added = 0; + + k=0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j=0; j < ranges[i].num_chars; ++j) { + int x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + if (glyph == 0 && (spc->skip_missing || missing_glyph_added)) { + rects[k].w = rects[k].h = 0; + } else { + stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + &x0,&y0,&x1,&y1); + rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); + rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + if (glyph == 0) + missing_glyph_added = 1; + } + ++k; + } + } + + return k; +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int prefilter_x, int prefilter_y, float *sub_x, float *sub_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, + output, + out_w - (prefilter_x - 1), + out_h - (prefilter_y - 1), + out_stride, + scale_x, + scale_y, + shift_x, + shift_y, + glyph); + + if (prefilter_x > 1) + stbtt__h_prefilter(output, out_w, out_h, out_stride, prefilter_x); + + if (prefilter_y > 1) + stbtt__v_prefilter(output, out_w, out_h, out_stride, prefilter_y); + + *sub_x = stbtt__oversample_shift(prefilter_x); + *sub_y = stbtt__oversample_shift(prefilter_y); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k, missing_glyph = -1, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h,recip_v,sub_x,sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j=0; j < ranges[i].num_chars; ++j) { + stbrp_rect *r = &rects[k]; + if (r->was_packed && r->w != 0 && r->h != 0) { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0,&y0,&x1,&y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w - spc->h_oversample+1, + r->h - spc->v_oversample+1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + + if (glyph == 0) + missing_glyph = j; + } else if (spc->skip_missing) { + return_value = 0; + } else if (r->was_packed && r->w == 0 && r->h == 0 && missing_glyph >= 0) { + ranges[i].chardata_for_range[j] = ranges[i].chardata_for_range[missing_glyph]; + } else { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; +} + +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) +{ + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); +} + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) +{ + stbtt_fontinfo info; + int i,j,n, return_value = 1; + //stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i=0; i < num_ranges; ++i) + for (j=0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; + + n = 0; + for (i=0; i < num_ranges; ++i) + n += ranges[i].num_chars; + + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; + + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + + stbtt_PackFontRangesPackRects(spc, rects, n); + + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; +} + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) +{ + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); +} + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap) +{ + int i_ascent, i_descent, i_lineGap; + float scale; + stbtt_fontinfo info; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata, index)); + scale = size > 0 ? stbtt_ScaleForPixelHeight(&info, size) : stbtt_ScaleForMappingEmToPixels(&info, -size); + stbtt_GetFontVMetrics(&info, &i_ascent, &i_descent, &i_lineGap); + *ascent = (float) i_ascent * scale; + *descent = (float) i_descent * scale; + *lineGap = (float) i_lineGap * scale; +} + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) +{ + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } else { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// sdf computation +// + +#define STBTT_min(a,b) ((a) < (b) ? (a) : (b)) +#define STBTT_max(a,b) ((a) < (b) ? (b) : (a)) + +static int stbtt__ray_intersect_bezier(float orig[2], float ray[2], float q0[2], float q1[2], float q2[2], float hits[2][2]) +{ + float q0perp = q0[1]*ray[0] - q0[0]*ray[1]; + float q1perp = q1[1]*ray[0] - q1[0]*ray[1]; + float q2perp = q2[1]*ray[0] - q2[0]*ray[1]; + float roperp = orig[1]*ray[0] - orig[0]*ray[1]; + + float a = q0perp - 2*q1perp + q2perp; + float b = q1perp - q0perp; + float c = q0perp - roperp; + + float s0 = 0., s1 = 0.; + int num_s = 0; + + if (a != 0.0) { + float discr = b*b - a*c; + if (discr > 0.0) { + float rcpna = -1 / a; + float d = (float) STBTT_sqrt(discr); + s0 = (b+d) * rcpna; + s1 = (b-d) * rcpna; + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + if (d > 0.0 && s1 >= 0.0 && s1 <= 1.0) { + if (num_s == 0) s0 = s1; + ++num_s; + } + } + } else { + // 2*b*s + c = 0 + // s = -c / (2*b) + s0 = c / (-2 * b); + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + } + + if (num_s == 0) + return 0; + else { + float rcp_len2 = 1 / (ray[0]*ray[0] + ray[1]*ray[1]); + float rayn_x = ray[0] * rcp_len2, rayn_y = ray[1] * rcp_len2; + + float q0d = q0[0]*rayn_x + q0[1]*rayn_y; + float q1d = q1[0]*rayn_x + q1[1]*rayn_y; + float q2d = q2[0]*rayn_x + q2[1]*rayn_y; + float rod = orig[0]*rayn_x + orig[1]*rayn_y; + + float q10d = q1d - q0d; + float q20d = q2d - q0d; + float q0rd = q0d - rod; + + hits[0][0] = q0rd + s0*(2.0f - 2.0f*s0)*q10d + s0*s0*q20d; + hits[0][1] = a*s0+b; + + if (num_s > 1) { + hits[1][0] = q0rd + s1*(2.0f - 2.0f*s1)*q10d + s1*s1*q20d; + hits[1][1] = a*s1+b; + return 2; + } else { + return 1; + } + } +} + +static int equal(float *a, float *b) +{ + return (a[0] == b[0] && a[1] == b[1]); +} + +static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex *verts) +{ + int i; + float orig[2], ray[2] = { 1, 0 }; + float y_frac; + int winding = 0; + + // make sure y never passes through a vertex of the shape + y_frac = (float) STBTT_fmod(y, 1.0f); + if (y_frac < 0.01f) + y += 0.01f; + else if (y_frac > 0.99f) + y -= 0.01f; + + orig[0] = x; + orig[1] = y; + + // test a ray from (-infinity,y) to (x,y) + for (i=0; i < nverts; ++i) { + if (verts[i].type == STBTT_vline) { + int x0 = (int) verts[i-1].x, y0 = (int) verts[i-1].y; + int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } + if (verts[i].type == STBTT_vcurve) { + int x0 = (int) verts[i-1].x , y0 = (int) verts[i-1].y ; + int x1 = (int) verts[i ].cx, y1 = (int) verts[i ].cy; + int x2 = (int) verts[i ].x , y2 = (int) verts[i ].y ; + int ax = STBTT_min(x0,STBTT_min(x1,x2)), ay = STBTT_min(y0,STBTT_min(y1,y2)); + int by = STBTT_max(y0,STBTT_max(y1,y2)); + if (y > ay && y < by && x > ax) { + float q0[2],q1[2],q2[2]; + float hits[2][2]; + q0[0] = (float)x0; + q0[1] = (float)y0; + q1[0] = (float)x1; + q1[1] = (float)y1; + q2[0] = (float)x2; + q2[1] = (float)y2; + if (equal(q0,q1) || equal(q1,q2)) { + x0 = (int)verts[i-1].x; + y0 = (int)verts[i-1].y; + x1 = (int)verts[i ].x; + y1 = (int)verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } else { + int num_hits = stbtt__ray_intersect_bezier(orig, ray, q0, q1, q2, hits); + if (num_hits >= 1) + if (hits[0][0] < 0) + winding += (hits[0][1] < 0 ? -1 : 1); + if (num_hits >= 2) + if (hits[1][0] < 0) + winding += (hits[1][1] < 0 ? -1 : 1); + } + } + } + } + return winding; +} + +static float stbtt__cuberoot( float x ) +{ + if (x<0) + return -(float) STBTT_pow(-x,1.0f/3.0f); + else + return (float) STBTT_pow( x,1.0f/3.0f); +} + +// x^3 + a*x^2 + b*x + c = 0 +static int stbtt__solve_cubic(float a, float b, float c, float* r) +{ + float s = -a / 3; + float p = b - a*a / 3; + float q = a * (2*a*a - 9*b) / 27 + c; + float p3 = p*p*p; + float d = q*q + 4*p3 / 27; + if (d >= 0) { + float z = (float) STBTT_sqrt(d); + float u = (-q + z) / 2; + float v = (-q - z) / 2; + u = stbtt__cuberoot(u); + v = stbtt__cuberoot(v); + r[0] = s + u + v; + return 1; + } else { + float u = (float) STBTT_sqrt(-p/3); + float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative + float m = (float) STBTT_cos(v); + float n = (float) STBTT_cos(v-3.141592/2)*1.732050808f; + r[0] = s + u * 2 * m; + r[1] = s - u * (m + n); + r[2] = s - u * (m - n); + + //STBTT_assert( STBTT_fabs(((r[0]+a)*r[0]+b)*r[0]+c) < 0.05f); // these asserts may not be safe at all scales, though they're in bezier t parameter units so maybe? + //STBTT_assert( STBTT_fabs(((r[1]+a)*r[1]+b)*r[1]+c) < 0.05f); + //STBTT_assert( STBTT_fabs(((r[2]+a)*r[2]+b)*r[2]+c) < 0.05f); + return 3; + } +} + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + float scale_x = scale, scale_y = scale; + int ix0,iy0,ix1,iy1; + int w,h; + unsigned char *data; + + if (scale == 0) return NULL; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale, scale, 0.0f,0.0f, &ix0,&iy0,&ix1,&iy1); + + // if empty, return NULL + if (ix0 == ix1 || iy0 == iy1) + return NULL; + + ix0 -= padding; + iy0 -= padding; + ix1 += padding; + iy1 += padding; + + w = (ix1 - ix0); + h = (iy1 - iy0); + + if (width ) *width = w; + if (height) *height = h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + // invert for y-downwards bitmaps + scale_y = -scale_y; + + { + // distance from singular values (in the same units as the pixel grid) + const float eps = 1./1024, eps2 = eps*eps; + int x,y,i,j; + float *precompute; + stbtt_vertex *verts; + int num_verts = stbtt_GetGlyphShape(info, glyph, &verts); + data = (unsigned char *) STBTT_malloc(w * h, info->userdata); + precompute = (float *) STBTT_malloc(num_verts * sizeof(float), info->userdata); + + for (i=0,j=num_verts-1; i < num_verts; j=i++) { + if (verts[i].type == STBTT_vline) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; + float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); + precompute[i] = (dist < eps) ? 0.0f : 1.0f / dist; + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; + float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; + float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float len2 = bx*bx + by*by; + if (len2 >= eps2) + precompute[i] = 1.0f / len2; + else + precompute[i] = 0.0f; + } else + precompute[i] = 0.0f; + } + + for (y=iy0; y < iy1; ++y) { + for (x=ix0; x < ix1; ++x) { + float val; + float min_dist = 999999.0f; + float sx = (float) x + 0.5f; + float sy = (float) y + 0.5f; + float x_gspace = (sx / scale_x); + float y_gspace = (sy / scale_y); + + int winding = stbtt__compute_crossings_x(x_gspace, y_gspace, num_verts, verts); // @OPTIMIZE: this could just be a rasterization, but needs to be line vs. non-tesselated curves so a new path + + for (i=0; i < num_verts; ++i) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + + if (verts[i].type == STBTT_vline && precompute[i] != 0.0f) { + float x1 = verts[i-1].x*scale_x, y1 = verts[i-1].y*scale_y; + + float dist,dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + // coarse culling against bbox + //if (sx > STBTT_min(x0,x1)-min_dist && sx < STBTT_max(x0,x1)+min_dist && + // sy > STBTT_min(y0,y1)-min_dist && sy < STBTT_max(y0,y1)+min_dist) + dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; + STBTT_assert(i != 0); + if (dist < min_dist) { + // check position along line + // x' = x0 + t*(x1-x0), y' = y0 + t*(y1-y0) + // minimize (x'-sx)*(x'-sx)+(y'-sy)*(y'-sy) + float dx = x1-x0, dy = y1-y0; + float px = x0-sx, py = y0-sy; + // minimize (px+t*dx)^2 + (py+t*dy)^2 = px*px + 2*px*dx*t + t^2*dx*dx + py*py + 2*py*dy*t + t^2*dy*dy + // derivative: 2*px*dx + 2*py*dy + (2*dx*dx+2*dy*dy)*t, set to 0 and solve + float t = -(px*dx + py*dy) / (dx*dx + dy*dy); + if (t >= 0.0f && t <= 1.0f) + min_dist = dist; + } + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[i-1].x *scale_x, y2 = verts[i-1].y *scale_y; + float x1 = verts[i ].cx*scale_x, y1 = verts[i ].cy*scale_y; + float box_x0 = STBTT_min(STBTT_min(x0,x1),x2); + float box_y0 = STBTT_min(STBTT_min(y0,y1),y2); + float box_x1 = STBTT_max(STBTT_max(x0,x1),x2); + float box_y1 = STBTT_max(STBTT_max(y0,y1),y2); + // coarse culling against bbox to avoid computing cubic unnecessarily + if (sx > box_x0-min_dist && sx < box_x1+min_dist && sy > box_y0-min_dist && sy < box_y1+min_dist) { + int num=0; + float ax = x1-x0, ay = y1-y0; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float mx = x0 - sx, my = y0 - sy; + float res[3] = {0.f,0.f,0.f}; + float px,py,t,it,dist2; + float a_inv = precompute[i]; + if (a_inv == 0.0) { // if a_inv is 0, it's 2nd degree so use quadratic formula + float a = 3*(ax*bx + ay*by); + float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); + float c = mx*ax+my*ay; + if (STBTT_fabs(a) < eps2) { // if a is 0, it's linear + if (STBTT_fabs(b) >= eps2) { + res[num++] = -c/b; + } + } else { + float discriminant = b*b - 4*a*c; + if (discriminant < 0) + num = 0; + else { + float root = (float) STBTT_sqrt(discriminant); + res[0] = (-b - root)/(2*a); + res[1] = (-b + root)/(2*a); + num = 2; // don't bother distinguishing 1-solution case, as code below will still work + } + } + } else { + float b = 3*(ax*bx + ay*by) * a_inv; // could precompute this as it doesn't depend on sample point + float c = (2*(ax*ax + ay*ay) + (mx*bx+my*by)) * a_inv; + float d = (mx*ax+my*ay) * a_inv; + num = stbtt__solve_cubic(b, c, d, res); + } + dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + if (num >= 1 && res[0] >= 0.0f && res[0] <= 1.0f) { + t = res[0], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 2 && res[1] >= 0.0f && res[1] <= 1.0f) { + t = res[1], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 3 && res[2] >= 0.0f && res[2] <= 1.0f) { + t = res[2], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + } + } + } + if (winding == 0) + min_dist = -min_dist; // if outside the shape, value is negative + val = onedge_value + pixel_dist_scale * min_dist; + if (val < 0) + val = 0; + else if (val > 255) + val = 255; + data[(y-iy0)*w+(x-ix0)] = (unsigned char) val; + } + } + STBTT_free(precompute, info->userdata); + STBTT_free(verts, info->userdata); + } + return data; +} + +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphSDF(info, scale, stbtt_FindGlyphIndex(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8); + stbtt_int32 off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + slen = ttUSHORT(fc+loc+12+8); + off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian_internal((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +static int stbtt_FindMatchingFont_internal(unsigned char *font_collection, char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, + float pixel_height, unsigned char *pixels, int pw, int ph, + int first_char, int num_chars, stbtt_bakedchar *chardata) +{ + return stbtt_BakeFontBitmap_internal((unsigned char *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata); +} + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) +{ + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); +} + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) +{ + return stbtt_GetNumberOfFonts_internal((unsigned char *) data); +} + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset) +{ + return stbtt_InitFont_internal(info, (unsigned char *) data, offset); +} + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags) +{ + return stbtt_FindMatchingFont_internal((unsigned char *) fontdata, (char *) name, flags); +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return stbtt_CompareUTF8toUTF16_bigendian_internal((char *) s1, len1, (char *) s2, len2); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) OpenType GPOS kerning (horizontal only), STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ From 97def6854e58ecac5670931f11ac7edc5f2551ec Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 16:05:40 -0400 Subject: [PATCH 13/19] capi/vector: SDF glyph atlas export + draw_text_str (GPU text, phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tile57_bake_glyph_sdf exports the SDF atlas (sprite_png) + a compact metrics JSON (sprite_json: em_px, pad, per-codepoint [u0,v0,u1,v1,off_x,off_y,w,h,advance] in EM units) for a host to load like the sprite atlas. VectorSurface.drawText now, when the host provides the new draw_text_str callback, sends the label as a STRING plus the alignment-applied baseline-left origin (px) and glyph pixel size — the host lays it out from the atlas metrics and draws SDF quads, instead of tile57 tessellating glyph outlines. Declutter now uses the real advance width. Falls back to draw_text (tessellation) when draw_text_str is null, so existing hosts are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 14 +++++++++++++- src/capi.zig | 44 ++++++++++++++++++++++++++++++++++++++++++ src/render/vector.zig | 45 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/include/tile57.h b/include/tile57.h index 096c1b5..8766492 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -196,6 +196,12 @@ int tile57_bake_assets(const char *catalog_dir, tile57_assets *out); * atlas (pivot-centred cells + {name:{x,y,width,height,pixelRatio}} JSON); other * fields are NULL. Free with tile57_assets_free. 1=ok, 0=error. */ int tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out); +/* SDF glyph atlas for GPU text: sprite_png is the RGBA signed-distance-field atlas + * of the label font; sprite_json is {"em_px","pad","glyphs":{codepoint:[u0,v0,u1, + * v1,off_x,off_y,w,h,advance]}} with the quad geometry in EM units (multiply by the + * text pixel size). A host draws each glyph as a textured quad sampling the SDF. + * Only sprite_* filled. Free with tile57_assets_free. 1=ok, 0=error. */ +int tile57_bake_glyph_sdf(tile57_assets *out); void tile57_assets_free(tile57_assets *out); /* Release a chart and all cached tiles. Must not be called while any renderer @@ -353,8 +359,14 @@ typedef struct { void (*draw_sprite)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, tile57_world_point anchor, float rot_deg, float half_w_px, float half_h_px); /* Area fill pattern: pattern name (ptr,len) to look up in the atlas ("pat:" * prefix) + the fill rings (world). Tile the cell across the polygon at a - * constant screen size. NULL => flat tint. Must be the LAST field. */ + * constant screen size. NULL => flat tint. */ void (*draw_pattern)(void *ctx, const tile57_feature *f, const char *name, size_t name_len, const tile57_world_rings *rings); + /* Text as a STRING for the host's SDF glyph atlas (tile57_bake_glyph_sdf): + * world anchor + the anchor-relative baseline-left origin in px (ox,oy, with + * alignment already applied) + UTF-8 text (ptr,len) + the glyph pixel size + + * colour + halo. The host lays the string out from its glyph metrics and draws + * SDF quads. NULL => text tessellates via draw_text. Must be the LAST field. */ + void (*draw_text_str)(void *ctx, const tile57_feature *f, tile57_world_point anchor, float ox_px, float oy_px, const char *text, size_t text_len, float size_px, tile57_rgba color, tile57_rgba halo); } tile57_surface_cb; /* Returns 0 ok / -1 bad handle / -2 render failure / -3 unsupported source. */ diff --git a/src/capi.zig b/src/capi.zig index 0eddb2c..d8765cc 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -650,6 +650,50 @@ export fn tile57_bake_sprite_mln(catalog_dir: ?[*:0]const u8, out: *CAssets) cal return 1; } +const glyph_sdf = @import("sprite").glyph; + +// Glyph metrics as compact JSON: {"em_px","pad","glyphs":{cp:[u0,v0,u1,v1,ox,oy,w,h,adv]}}. +fn glyphMetricsJson(a: std.mem.Allocator, atlas: *const glyph_sdf.Atlas) ![]u8 { + var out = std.ArrayList(u8).empty; + try out.print(a, "{{\"em_px\":{d},\"pad\":{d},\"glyphs\":{{", .{ atlas.em_px, atlas.pad }); + var it = atlas.glyphs.iterator(); + var first = true; + while (it.next()) |e| { + const g = e.value_ptr.*; + if (!first) try out.append(a, ','); + first = false; + try out.print(a, "\"{d}\":[{d},{d},{d},{d},{d},{d},{d},{d},{d}]", .{ e.key_ptr.*, g.u0, g.v0, g.u1, g.v1, g.off_x, g.off_y, g.w, g.h, g.advance }); + } + try out.appendSlice(a, "}}"); + return out.toOwnedSlice(a); +} + +/// SDF glyph atlas for GPU text: sprite_png = the RGBA SDF atlas, sprite_json = +/// {"em_px","pad","glyphs":{codepoint:[u0,v0,u1,v1,ox,oy,w,h,adv]}} (EM units). +/// Only sprite_* filled. Free with tile57_assets_free. 1=ok, 0=error. See tile57.h. +export fn tile57_bake_glyph_sdf(out: *CAssets) callconv(.c) c_int { + out.* = .{}; + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const font = @import("render").font.notosans; + const cps = glyph_sdf.defaultCodepoints(a) catch return 0; + var atlas = glyph_sdf.build(a, font, cps, 32.0, 6) catch return 0; + const png = (atlas.encodePng(a) catch return 0) orelse return 0; + const json = glyphMetricsJson(a, &atlas) catch return 0; + out.sprite_png = (gpa.dupe(u8, png) catch { + tile57_assets_free(out); + return 0; + }).ptr; + out.sprite_png_len = png.len; + out.sprite_json = (gpa.dupe(u8, json) catch { + tile57_assets_free(out); + return 0; + }).ptr; + out.sprite_json_len = json.len; + return 1; +} + /// Free every non-null buffer in *out and zero the struct. See tile57.h. export fn tile57_assets_free(out: *CAssets) callconv(.c) void { if (out.colortables) |p| chart.freeBytes(p[0..out.colortables_len]); diff --git a/src/render/vector.zig b/src/render/vector.zig index 8fdb4b2..76c260d 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -91,6 +91,12 @@ pub const CSurface = extern struct { /// prefix) + the fill rings (world). Tile the pattern cell across the polygon /// at a constant screen size. Null => patterns fall back to a flat tint. draw_pattern: ?*const fn (?*anyopaque, *const CFeature, [*]const u8, usize, *const CWorldRings) callconv(.c) void = null, + /// Text label as a STRING for the host's SDF glyph atlas: world anchor + the + /// anchor-relative baseline-left origin in px (ox,oy, alignment already applied) + /// + UTF-8 text (ptr,len) + the glyph pixel size + colour + halo. The host lays + /// the string out from its glyph metrics and draws SDF quads. Null => text + /// tessellates via draw_text instead. Must be the LAST field (ABI-appended). + draw_text_str: ?*const fn (?*anyopaque, *const CFeature, CWorldPt, f32, f32, [*]const u8, usize, f32, CColor, CColor) callconv(.c) void = null, }; /// Greedy screen-box occupancy for label/symbol declutter. Features arrive in @@ -410,15 +416,42 @@ pub const VectorSurface = struct { const self = sp(ctx); if (!self.cur_visible) return; if (!resolve.textGroupVisible(style.group, self.settings)) return; + const f = &(self.fnt orelse return); const font_css: f32 = @floatCast(if (style.font_size > 0) style.font_size else 12); - // Declutter: estimate the label box (width ~ chars × 0.55 em) and yield - // if it collides with a higher-priority label/symbol already placed. - const px = @as(f64, font_css) * self.refDev(); - const w = @as(f64, @floatFromInt(text.len)) * px * 0.55; + const px = font_css * @as(f32, @floatCast(self.refDev())); + if (px <= 1) return; + + // Real advance width — the declutter box and (SDF path) the alignment. + var pen: f32 = 0; + var it = (std.unicode.Utf8View.init(text) catch return).iterator(); + while (it.nextCodepoint()) |cp| pen += f.advance(f.glyphIndex(cp)) * px; + if (pen <= 0) return; + const anchor = self.worldOf(at); const scale_px = 256.0 * std.math.exp2(self.view_zoom); - if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, w * 0.5, px * 0.6, false)) return; - try self.emitText(text, font_css, if (style.halign.len > 0) style.halign else "center", if (style.valign.len > 0) style.valign else "middle", @floatCast(style.offset_x), @floatCast(style.offset_y), self.resolveColor(style.color), at); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, @as(f64, pen) * 0.5, @as(f64, px) * 0.6, false)) return; + + const halign = if (style.halign.len > 0) style.halign else "center"; + const valign = if (style.valign.len > 0) style.valign else "middle"; + // SDF glyph-atlas host: send the string + aligned baseline-left origin. + if (self.cb.draw_text_str) |dts| { + const mm_px: f32 = @floatCast(sndfrm.SYMBOL_SCALE * 100.0 * self.refDev()); + var x0: f32 = @as(f32, @floatCast(style.offset_x)) * mm_px; + if (std.mem.eql(u8, halign, "center")) x0 -= pen / 2; + if (std.mem.eql(u8, halign, "right")) x0 -= pen; + var baseline: f32 = @as(f32, @floatCast(style.offset_y)) * mm_px; + if (std.mem.eql(u8, valign, "top")) { + baseline += f.ascent * px; + } else if (std.mem.eql(u8, valign, "middle")) { + baseline += (f.ascent - f.descent) / 2 * px; + } else { + baseline -= f.descent * px; + } + const feat = self.cur_feature(); + dts(self.cb.ctx, &feat, anchor, x0, baseline, text.ptr, text.len, px, ccolor(self.resolveColor(style.color)), .{ .r = 0, .g = 0, .b = 0, .a = 0 }); + return; + } + try self.emitText(text, font_css, halign, valign, @floatCast(style.offset_x), @floatCast(style.offset_y), self.resolveColor(style.color), at); } fn glyphOutline(self: *VectorSurface, gid: u16) ![]const []const cv.Point { From 62996c5a45b6f24e8567ab55131b87795edcbd8a Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 17:22:26 -0400 Subject: [PATCH 14/19] chart: live single-cell open (.cell) + M_COVR coverage C API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tile57_chart_open now opens a single .000 (+ its .001.. update chain) as a live .cell backend — the one render_surface_cb supports — instead of the lazy .cells backend (bake-only, so render_surface_cb returned -3). Add tile57_charts_open for the ENC_ROOT / lazy path. CellBackend now assembles the cell's M_COVR(CATCOV=1) data-coverage polygons, and tile57_chart_coverage reports them (exterior ring as interleaved lon,lat doubles) so a host can quilt gaps to coarser cells instead of over-claiming the bounding box. Verified: opening US5MD1MC.000 renders (34 fills) and returns its coverage (1 polygon, 123 points). Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 25 ++++++++++++++---- src/capi.zig | 41 +++++++++++++++++++++++++++++ src/chart.zig | 67 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/include/tile57.h b/include/tile57.h index 8766492..c4fd6ed 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -75,13 +75,17 @@ typedef struct { * (the zero-initialised default) keeps them — so existing callers that pass 0 get * the pick report for free. */ -/* Open an on-disk ENC_ROOT directory (or a single .000 file) as a streaming chart: - * the engine enumerates the cells + peeks each one's bbox/scale at open, then reads - * cell bytes on demand (working set only), so RSS tracks what tiles need, not the - * whole ENC_ROOT. Rules are the library's embedded catalogue. NULL/failure -> NULL. - * */ +/* Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the + * same directory) as a LIVE chart: portrayed on open, then rendered per view via + * tile57_chart_render_surface_cb, with real M_COVR coverage from tile57_chart_coverage. + * Rules are the library's embedded catalogue. NULL/failure -> NULL. */ tile57_chart *tile57_chart_open(const char *path); +/* Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked streaming + * chart: enumerates cells + peeks each bbox/scale, reads bytes on demand (working + * set only). For tile fetch / bake, NOT live render_surface_cb. NULL/failure -> NULL. */ +tile57_chart *tile57_charts_open(const char *path); + /* Open one in-memory ENC cell (base .000 bytes) as a resident chart. Bytes are copied. * NULL/failure -> NULL. */ tile57_chart *tile57_chart_open_bytes(const uint8_t *base, size_t len); @@ -391,6 +395,17 @@ typedef struct { int tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom, const tile57_query_cb *cb); +/* The chart's M_COVR(CATCOV=1) data-coverage polygons — the real coverage a host + * reports so a quilt fills gaps to coarser cells (vs. the bounding box). ring() is + * called once per polygon with its exterior ring as `npts` interleaved lon,lat + * doubles (valid only during the call). Only the live-cell backend (an opened + * .000) carries this; a baked PMTiles returns 0 with no calls. 0 ok, -1 bad args. */ +typedef struct { + void *ctx; + void (*ring)(void *ctx, const double *lonlat, size_t npts); +} tile57_coverage_cb; +int tile57_chart_coverage(tile57_chart *chart, const tile57_coverage_cb *cb); + /* The chart's per-cell metadata as a JSON array, one object per cell: * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", * "issueDate":"20240105","agency":550,"bbox":[west,south,east,north]}, ...] diff --git a/src/capi.zig b/src/capi.zig index d8765cc..bea47e3 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -69,7 +69,18 @@ fn toCellInputs(a: std.mem.Allocator, c_cells: []const CellInput) ?[]chart.CellI } /// Open an on-disk ENC_ROOT directory (or single .000) as a streaming chart. See tile57.h. +/// Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the +/// same directory) as a live chart — a `.cell` backend that render_surface_cb and +/// tile57_chart_coverage support. For a whole ENC_ROOT directory use +/// tile57_charts_open. See tile57.h. export fn tile57_chart_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { + const p = spanOpt(path) orelse return null; + return chart.openCellPath(p, null, true) catch null; +} + +/// Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked chart — the +/// `.cells` backend, for tile fetch / bake, not live render_surface_cb. See tile57.h. +export fn tile57_charts_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { const p = spanOpt(path) orelse return null; return Chart.openPath(p, null, true) catch null; } @@ -160,6 +171,36 @@ export fn tile57_chart_query(handle: ?*Chart, lon: f64, lat: f64, zoom: f64, cb: return 0; } +const CCoverageCb = extern struct { + ctx: ?*anyopaque, + ring: *const fn (?*anyopaque, lonlat: [*]const f64, npts: usize) callconv(.c) void, +}; + +/// The chart's M_COVR data-coverage polygons (live-cell backend only): cb->ring is +/// called once per polygon with its exterior ring as interleaved lon,lat doubles. +/// A baked PMTiles chart carries no coverage polygon (returns 0, no calls). +/// 0 ok, -1 bad args. See tile57.h. +export fn tile57_chart_coverage(handle: ?*Chart, cb: ?*const CCoverageCb) callconv(.c) c_int { + const self = handle orelse return -1; + const cbp = cb orelse return -1; + const polys = self.coverage() orelse return 0; + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + for (polys) |poly| { + if (poly.len == 0) continue; + const ring = poly[0]; // exterior ring + if (ring.len < 3) continue; + const out = a.alloc(f64, ring.len * 2) catch continue; + for (ring, 0..) |p, i| { + out[2 * i] = @as(f64, @floatFromInt(p.lon_e7)) / 1e7; + out[2 * i + 1] = @as(f64, @floatFromInt(p.lat_e7)) / 1e7; + } + cbp.ring(cbp.ctx, out.ptr, ring.len); + } + return 0; +} + // Progress callback for tile57_bake_pmtiles / tile57_bake_bundle (matches the header // typedef + chart.Progress + bake_enc.Progress). const BakeProgress = ?*const fn (user: ?*anyopaque, stage: u8, done: usize, total: usize, band_index: u8, band_count: u8, band_name: ?[*:0]const u8) callconv(.c) void; diff --git a/src/chart.zig b/src/chart.zig index e3fd521..19e4703 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -102,6 +102,7 @@ const CellBackend = struct { portrayal_plain: ?[]const ?[]const u8 = null, // PlainBoundaries variant (areas) portrayal_simplified: ?[]const ?[]const u8 = null, // SimplifiedSymbols variant (points) portray_arena: ?*std.heap.ArenaAllocator = null, + coverage: []const []const []const s57.LonLat = &.{}, // M_COVR (in portray_arena) }; // One cell in the lazy ENC_ROOT index: its owned bytes + cheap metadata (bbox + @@ -435,15 +436,14 @@ fn buildCellBackend(base: []const u8, updates: []const []const u8, dir: []const var cb = CellBackend{ .cell = cell }; const pa = gpa.create(std.heap.ArenaAllocator) catch return cb; pa.* = std.heap.ArenaAllocator.init(gpa); + cb.portray_arena = pa; + // Real M_COVR data-coverage polygons for the host to report as chart coverage. + cb.coverage = cb.cell.mcovrCoverage(pa.allocator()); if (portray.portrayCellVariants(pa.allocator(), &cb.cell, dir)) |cp| { cb.portrayal = cp.base; cb.portrayal_plain = cp.plain; cb.portrayal_simplified = cp.simplified; - cb.portray_arena = pa; - } else |_| { - pa.deinit(); - gpa.destroy(pa); - } + } else |_| {} return cb; } @@ -465,6 +465,52 @@ fn openCell(bytes: []const u8, rules_dir: ?[]const u8) ?*Chart { return src; } +/// Open a SINGLE .000 cell (+ its sequential .001.. update chain, read from the +/// cell's directory) as a live `.cell` chart — the backend render_surface_cb and +/// coverage() support (unlike the lazy `.cells` backend openPath builds, which is +/// bake-only). For a whole ENC_ROOT directory use openPath. +pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { + const threaded = try gpa.create(std.Io.Threaded); + threaded.* = .init(gpa, .{}); + defer { + threaded.deinit(); + gpa.destroy(threaded); + } + const io = threaded.io(); + const dir_path = std.fs.path.dirname(path) orelse "."; + var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{}); + defer dir.close(io); + + const bn = std.fs.path.basename(path); + const base = try dir.readFileAlloc(io, bn, gpa, .unlimited); + defer gpa.free(base); + var updates = std.ArrayList([]u8).empty; + defer { + for (updates.items) |u| gpa.free(u); + updates.deinit(gpa); + } + if (bn.len > 4) { + const stem = bn[0 .. bn.len - 4]; // strip ".000" + var u: u32 = 1; + while (u <= 999) : (u += 1) { + const upn = std.fmt.allocPrint(gpa, "{s}.{d:0>3}", .{ stem, u }) catch break; + defer gpa.free(upn); + const ub = dir.readFileAlloc(io, upn, gpa, .unlimited) catch break; + updates.append(gpa, ub) catch { + gpa.free(ub); + break; + }; + } + } + var cb = buildCellBackend(base, updates.items, resolveRulesDir(rules_dir)) orelse return error.OpenFailed; + const src = gpa.create(Chart) catch { + freeCellBackend(&cb); + return error.OpenFailed; + }; + src.* = .{ .backend = .{ .cell = cb }, .data = null, .cache = std.AutoHashMap(u64, []u8).init(gpa), .pick_attrs = pick_attrs }; + return src; +} + // Parallel open worker: peek each cell's band + bbox and copy its bytes. const OpenWork = struct { inputs: []const CellInput, @@ -700,6 +746,17 @@ pub const Chart = struct { }; } + /// The cell's M_COVR(CATCOV=1) data-coverage polygons (polygon -> rings -> + /// lon/lat points), for the host to report as chart coverage so OpenCPN quilts + /// gaps to coarser cells. Only the live-cell backend has it (a baked PMTiles + /// carries no coverage polygon); null otherwise. + pub fn coverage(self: *const Chart) ?[]const []const []const s57.LonLat { + return switch (self.backend) { + .cell => |*c| if (c.coverage.len > 0) c.coverage else null, + else => null, + }; + } + /// The encoding `tile` returns: a PMTiles backend reports its archive's stored /// tile type (tiles serve verbatim); a cell backend reports its live /// generation format (`tile_format`). Non-vector archive types (png/…) are From 9c67a858f1f44b8b096cc583635cea9c3ba8779a Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 18:22:33 -0400 Subject: [PATCH 15/19] chart: bake single cells to in-memory PMTiles + header/zoom opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A live cell re-portrayed per view is ~1500ms/frame (every tile re-parses S-101 instructions). Instead, tile57_chart_open now bakes the cell to an in-memory PMTiles ONCE and serves it via the fast .reader path (8-12ms/view, a ~150x speedup), with the cell's real M_COVR coverage and compilation scale (CSCL) captured from the parse and attached (coverage_override / cscl_override) since the baked tiles carry neither. For a host to stay responsive: - tile57_chart_open_header: metadata only (bbox + native_scale + coverage) via a cheap parse, no bake (~18ms) — for a chart-DB/header scan that must not bake every cell. - tile57_chart_open_zoom(min,max): bake a narrow band; a host bakes an overview fast for first paint then re-opens the full range in the background (progressive load). Adds native_scale to tile57_chart_info; keeps the .cell backend (openCell / raw bytes) with the geo cache for its live path. Verified: header 18ms, full bake 1859ms, render 8-12ms, coverage 1 polygon / 123 pts, native_scale 1:12000. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 20 ++++++-- src/capi.zig | 25 ++++++++-- src/chart.zig | 126 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 154 insertions(+), 17 deletions(-) diff --git a/include/tile57.h b/include/tile57.h index c4fd6ed..f5401dd 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -76,11 +76,23 @@ typedef struct { * the pick report for free. */ /* Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the - * same directory) as a LIVE chart: portrayed on open, then rendered per view via - * tile57_chart_render_surface_cb, with real M_COVR coverage from tile57_chart_coverage. - * Rules are the library's embedded catalogue. NULL/failure -> NULL. */ + * same directory) by BAKING it to an in-memory PMTiles and serving the fast reader + * render path (tile57_chart_render_surface_cb), with the cell's real M_COVR coverage + * (tile57_chart_coverage) and compilation scale (chart_info.native_scale) attached. + * The bake costs ~1-2s; a host rendering per view should run it off-thread. See the + * header/zoom variants for a cheap scan pass + progressive load. NULL/fail -> NULL. */ tile57_chart *tile57_chart_open(const char *path); +/* Open ONE cell for METADATA ONLY — bbox, native_scale, and M_COVR coverage — via a + * cheap parse with NO tile bake, for a host's chart-database/header scan. Do NOT + * render_surface this handle (it has no portrayal). NULL/failure -> NULL. */ +tile57_chart *tile57_chart_open_header(const char *path); + +/* Open ONE cell baking only [minzoom, maxzoom] to an in-memory PMTiles: bake a narrow + * native band fast for first paint, then re-open the full range in the background + * (progressive load). Renders via the fast reader path. NULL/failure -> NULL. */ +tile57_chart *tile57_chart_open_zoom(const char *path, uint8_t minzoom, uint8_t maxzoom); + /* Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked streaming * chart: enumerates cells + peeks each bbox/scale, reads bytes on demand (working * set only). For tile fetch / bake, NOT live render_surface_cb. NULL/failure -> NULL. */ @@ -110,6 +122,8 @@ typedef struct { bool has_bounds; double west, south, east, north; bool has_anchor; double anchor_lat, anchor_lon, anchor_zoom; uint8_t tile_type; /* tile57_tile_type */ + int32_t native_scale; /* compilation scale (1:N) for a live cell; 0 = unknown + * (PMTiles: derive the scale from the zoom band instead) */ } tile57_chart_info; void tile57_chart_get_info(tile57_chart *chart, tile57_chart_info *out); diff --git a/src/capi.zig b/src/capi.zig index bea47e3..405d441 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -68,16 +68,31 @@ fn toCellInputs(a: std.mem.Allocator, c_cells: []const CellInput) ?[]chart.CellI return out; } -/// Open an on-disk ENC_ROOT directory (or single .000) as a streaming chart. See tile57.h. /// Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the -/// same directory) as a live chart — a `.cell` backend that render_surface_cb and -/// tile57_chart_coverage support. For a whole ENC_ROOT directory use -/// tile57_charts_open. See tile57.h. +/// same directory) by baking it to an in-memory PMTiles and serving the fast reader +/// path — with the cell's real M_COVR coverage + compilation scale attached. For a +/// whole ENC_ROOT directory use tile57_charts_open. See tile57.h. export fn tile57_chart_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { const p = spanOpt(path) orelse return null; return chart.openCellPath(p, null, true) catch null; } +/// Open ONE cell for METADATA ONLY (bbox + native scale + M_COVR coverage): a cheap +/// parse, no tile bake — for a host's header/scan pass. Do NOT render_surface this +/// handle (no portrayal). See tile57.h. +export fn tile57_chart_open_header(path: ?[*:0]const u8) callconv(.c) ?*Chart { + const p = spanOpt(path) orelse return null; + return chart.openCellHeader(p, null, true) catch null; +} + +/// Open ONE cell by baking only [minzoom, maxzoom] to an in-memory PMTiles — a host +/// bakes a narrow band fast for first paint, then re-opens the full range in the +/// background (progressive load). Renders via the fast reader path. See tile57.h. +export fn tile57_chart_open_zoom(path: ?[*:0]const u8, minzoom: u8, maxzoom: u8) callconv(.c) ?*Chart { + const p = spanOpt(path) orelse return null; + return chart.openCellBaked(p, null, true, minzoom, maxzoom) catch null; +} + /// Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked chart — the /// `.cells` backend, for tile fetch / bake, not live render_surface_cb. See tile57.h. export fn tile57_charts_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { @@ -125,6 +140,7 @@ const CChartInfo = extern struct { // backend reports its archive's stored type; a cell backend its live // generation format. Appended for ABI-append-safety. tile_type: u8, + native_scale: i32, // live cell compilation scale (1:N); 0 = derive from zoom }; // tile57_tile_type values (keep in sync with tile57.h). @@ -139,6 +155,7 @@ export fn tile57_chart_get_info(src: ?*Chart, out: *CChartInfo) callconv(.c) voi const zr = s.zoomRange(); out.min_zoom = zr.min; out.max_zoom = zr.max; + out.native_scale = s.nativeScale(); out.bands = s.bands(); out.tile_type = switch (s.tileType()) { .mlt => TILE_TYPE_MLT, diff --git a/src/chart.zig b/src/chart.zig index 19e4703..e624554 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -103,6 +103,13 @@ const CellBackend = struct { portrayal_simplified: ?[]const ?[]const u8 = null, // SimplifiedSymbols variant (points) portray_arena: ?*std.heap.ArenaAllocator = null, coverage: []const []const []const s57.LonLat = &.{}, // M_COVR (in portray_arena) + cscl: i32 = 0, // compilation scale (DSPM CSCL, 1:N) + // Baker-style per-cell caches (in portray_arena), built once at open so each of + // the view's tiles reuses them instead of re-assembling geometry + re-projecting + // + re-processing every feature every rebuild (see renderSurfaceView's .cell arm). + geo: ?scene.GeoParts = null, // assembled ring geometry + geo_world: ?scene.GeoWorld = null, // its web-mercator projection + feat_bbox: ?[]const ?[4]f64 = null, // per-feature lon/lat bbox (spatial cull) }; // One cell in the lazy ENC_ROOT index: its owned bytes + cheap metadata (bbox + @@ -433,7 +440,7 @@ fn openPmtiles(copy: []u8) ?*Chart { // but does not take ownership. Portrayal failure is non-fatal (classify() fallback). fn buildCellBackend(base: []const u8, updates: []const []const u8, dir: []const u8) ?CellBackend { const cell = s57.parseCellWithUpdates(gpa, base, updates) catch return null; - var cb = CellBackend{ .cell = cell }; + var cb = CellBackend{ .cell = cell, .cscl = s57.peekScale(gpa, base) orelse 0 }; const pa = gpa.create(std.heap.ArenaAllocator) catch return cb; pa.* = std.heap.ArenaAllocator.init(gpa); cb.portray_arena = pa; @@ -444,6 +451,14 @@ fn buildCellBackend(base: []const u8, updates: []const []const u8, dir: []const cb.portrayal_plain = cp.plain; cb.portrayal_simplified = cp.simplified; } else |_| {} + // Assemble geometry + its projection + per-feature bboxes ONCE (the baker's + // per-cell caches) so live per-view rendering reuses them across the view's tiles + // instead of re-assembling + re-projecting + re-processing every feature per tile. + if (scene.buildGeoCache(pa.allocator(), &cb.cell)) |g| { + cb.geo = g; + cb.geo_world = scene.buildGeoWorld(pa.allocator(), g) catch null; + cb.feat_bbox = scene.buildFeatBBox(pa.allocator(), &cb.cell, g) catch null; + } else |_| {} return cb; } @@ -465,11 +480,19 @@ fn openCell(bytes: []const u8, rules_dir: ?[]const u8) ?*Chart { return src; } -/// Open a SINGLE .000 cell (+ its sequential .001.. update chain, read from the -/// cell's directory) as a live `.cell` chart — the backend render_surface_cb and -/// coverage() support (unlike the lazy `.cells` backend openPath builds, which is -/// bake-only). For a whole ENC_ROOT directory use openPath. -pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { +/// A single ENC cell's on-disk bytes: base .000 + its sequential .001.. update chain. +const CellFiles = struct { + base: []u8, + updates: [][]u8, + fn deinit(self: *CellFiles) void { + gpa.free(self.base); + for (self.updates) |u| gpa.free(u); + gpa.free(self.updates); + } +}; + +/// Read a .000 cell + its .001.. updates from the cell's directory into gpa buffers. +fn readCellFiles(path: []const u8) !CellFiles { const threaded = try gpa.create(std.Io.Threaded); threaded.* = .init(gpa, .{}); defer { @@ -483,9 +506,9 @@ pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) const bn = std.fs.path.basename(path); const base = try dir.readFileAlloc(io, bn, gpa, .unlimited); - defer gpa.free(base); + errdefer gpa.free(base); var updates = std.ArrayList([]u8).empty; - defer { + errdefer { for (updates.items) |u| gpa.free(u); updates.deinit(gpa); } @@ -502,15 +525,72 @@ pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) }; } } - var cb = buildCellBackend(base, updates.items, resolveRulesDir(rules_dir)) orelse return error.OpenFailed; + return .{ .base = base, .updates = try updates.toOwnedSlice(gpa) }; +} + +/// Open a SINGLE .000 cell (+ updates) for its METADATA ONLY — bbox, compilation +/// scale, and M_COVR coverage — via a cheap parse (no portrayal, no geometry cache, +/// no tile bake). For the host's header/scan pass, where nothing renders. The +/// resulting `.cell` chart must NOT be render_surface'd (it has no portrayal). +pub fn openCellHeader(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { + _ = rules_dir; + var cf = try readCellFiles(path); + defer cf.deinit(); + const cell = s57.parseCellWithUpdates(gpa, cf.base, cf.updates) catch return error.OpenFailed; + var cb = CellBackend{ .cell = cell, .cscl = s57.peekScale(gpa, cf.base) orelse 0 }; + const pa = gpa.create(std.heap.ArenaAllocator) catch { + freeCellBackend(&cb); + return error.OpenFailed; + }; + pa.* = std.heap.ArenaAllocator.init(gpa); + cb.portray_arena = pa; + cb.coverage = cb.cell.mcovrCoverage(pa.allocator()); const src = gpa.create(Chart) catch { freeCellBackend(&cb); return error.OpenFailed; }; - src.* = .{ .backend = .{ .cell = cb }, .data = null, .cache = std.AutoHashMap(u64, []u8).init(gpa), .pick_attrs = pick_attrs }; + src.* = .{ .backend = .{ .cell = cb }, .cache = std.AutoHashMap(u64, []u8).init(gpa), .pick_attrs = pick_attrs }; + return src; +} + +/// Open a SINGLE .000 cell (+ updates) by BAKING it to an in-memory PMTiles across +/// [minzoom, maxzoom] and serving it via the fast `.reader` path — a live cell +/// re-portrayed per view is orders of magnitude slower. The baked tiles carry no +/// M_COVR / CSCL, so the source cell's real coverage + compilation scale are captured +/// and attached (coverage()/nativeScale()). A host can bake a narrow band quickly +/// then re-open the full range in the background (progressive load). +pub fn openCellBaked(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool, minzoom: u8, maxzoom: u8) !*Chart { + var cf = try readCellFiles(path); + defer cf.deinit(); + const cscl = s57.peekScale(gpa, cf.base) orelse 0; + var cov_arena = try gpa.create(std.heap.ArenaAllocator); + cov_arena.* = std.heap.ArenaAllocator.init(gpa); + errdefer { + cov_arena.deinit(); + gpa.destroy(cov_arena); + } + var coverage: []const []const []const s57.LonLat = &.{}; + if (s57.parseCellWithUpdates(gpa, cf.base, cf.updates)) |parsed| { + var cell = parsed; + coverage = cell.mcovrCoverage(cov_arena.allocator()); // assembled into cov_arena + cell.deinit(); + } else |_| {} + + const cell_in = [_]CellInput{.{ .base = cf.base, .updates = cf.updates }}; + const archive = (bakeArchive(&cell_in, resolveRulesDir(rules_dir), minzoom, maxzoom, .mlt, pick_attrs, null, null) catch null) orelse return error.OpenFailed; + defer gpa.free(archive); + const src = try Chart.openBytes(archive, .pmtiles, rules_dir); + src.cscl_override = cscl; + src.coverage_override = coverage; + src.coverage_arena = cov_arena; return src; } +/// Bake the full zoom range (the default single-cell open). +pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { + return openCellBaked(path, rules_dir, pick_attrs, 0, 18); +} + // Parallel open worker: peek each cell's band + bbox and copy its bytes. const OpenWork = struct { inputs: []const CellInput, @@ -564,6 +644,13 @@ pub const Chart = struct { // backend ignores it — stored tiles serve verbatim in their baked encoding // (see tileType). tile_format: scene.TileFormat = .mvt, + // A live cell baked to an in-memory PMTiles (openCellBaked) is a .reader for + // fast render, but still carries the source cell's real M_COVR coverage and + // compilation scale (the baked tiles don't). These override coverage()/ + // nativeScale(); coverage_arena owns the copied polygons (freed in deinit). + coverage_override: []const []const []const s57.LonLat = &.{}, + coverage_arena: ?*std.heap.ArenaAllocator = null, + cscl_override: i32 = 0, /// Open a source from in-memory bytes. `fmt` selects the backend (`.auto` /// sniffs PMTiles then S-57); `rules_dir` is the S-101 rules dir for cells @@ -735,6 +822,10 @@ pub const Chart = struct { while (it.next()) |v| gpa.free(v.*); self.cache.deinit(); if (self.data) |d| gpa.free(d); + if (self.coverage_arena) |ca| { + ca.deinit(); + gpa.destroy(ca); + } gpa.destroy(self); } @@ -751,12 +842,24 @@ pub const Chart = struct { /// gaps to coarser cells. Only the live-cell backend has it (a baked PMTiles /// carries no coverage polygon); null otherwise. pub fn coverage(self: *const Chart) ?[]const []const []const s57.LonLat { + if (self.coverage_override.len > 0) return self.coverage_override; return switch (self.backend) { .cell => |*c| if (c.coverage.len > 0) c.coverage else null, else => null, }; } + /// The cell's compilation scale (DSPM CSCL, 1:N) — the live-cell chart's native + /// scale, so the host doesn't derive an over-detailed one from the 0..18 zoom + /// range. 0 (unknown) for a PMTiles chart (derive from the zoom band instead). + pub fn nativeScale(self: *const Chart) i32 { + if (self.cscl_override != 0) return self.cscl_override; + return switch (self.backend) { + .cell => |*c| c.cscl, + else => 0, + }; + } + /// The encoding `tile` returns: a PMTiles backend reports its archive's stored /// tile type (tiles serve verbatim); a cell backend reports its live /// generation format (`tile_format`). Non-vector archive types (png/…) are @@ -1058,6 +1161,9 @@ pub const Chart = struct { .portrayal = cb2.portrayal, .portrayal_plain = cb2.portrayal_plain, .portrayal_simplified = cb2.portrayal_simplified, + .geo = cb2.geo, + .geo_world = cb2.geo_world, + .feat_bbox = cb2.feat_bbox, }}; while (vt.next()) |t| { vs.setTile(t.z, t.x, t.y); From aa005d10b29c96a6dcce2537468e9c76d61a6549 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 18:47:45 -0400 Subject: [PATCH 16/19] chart: tile57_warmup() to pre-populate global registries for threaded hosts A host that bakes/renders cells from multiple threads needs the two process-global read-only registries (S-100 catalogue + complex linestyles) fully populated first, since their lazy init isn't itself synchronized. warmup() does that on the calling thread; thereafter both are read-only, so with the thread-safe smp allocator and the threadlocal portrayal context, concurrent bake/render is race-free. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 8 ++++++++ src/capi.zig | 7 +++++++ src/chart.zig | 14 ++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/include/tile57.h b/include/tile57.h index f5401dd..15de1aa 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -98,6 +98,14 @@ tile57_chart *tile57_chart_open_zoom(const char *path, uint8_t minzoom, uint8_t * set only). For tile fetch / bake, NOT live render_surface_cb. NULL/failure -> NULL. */ tile57_chart *tile57_charts_open(const char *path); +/* Populate the process-global read-only registries (the S-100 feature catalogue and + * the complex-linestyle table) on the calling thread. Both are idempotent lazy-init + * and thereafter read-only. Call this ONCE on your main thread before opening or + * baking cells from worker threads, so those globals are fully populated first and + * concurrent bake/render is race-free (the allocator is thread-safe and the portrayal + * context is thread-local). Cheap and safe to call more than once. */ +void tile57_warmup(void); + /* Open one in-memory ENC cell (base .000 bytes) as a resident chart. Bytes are copied. * NULL/failure -> NULL. */ tile57_chart *tile57_chart_open_bytes(const uint8_t *base, size_t len); diff --git a/src/capi.zig b/src/capi.zig index 405d441..2e3c8a3 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -100,6 +100,13 @@ export fn tile57_charts_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { return Chart.openPath(p, null, true) catch null; } +/// Populate the process-global read-only registries (S-100 catalogue + linestyles) on +/// the calling thread. Call ONCE on the main thread before opening/baking cells from +/// worker threads, so concurrent bake/render is race-free. See tile57.h. +export fn tile57_warmup() callconv(.c) void { + chart.warmup(); +} + /// Open one in-memory ENC cell (base .000 bytes) as a resident chart. See tile57.h. export fn tile57_chart_open_bytes(base: [*]const u8, len: usize) callconv(.c) ?*Chart { if (len == 0) return null; diff --git a/src/chart.zig b/src/chart.zig index e624554..de3c82d 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -591,6 +591,20 @@ pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) return openCellBaked(path, rules_dir, pick_attrs, 0, 18); } +/// Populate the process-global READ-ONLY registries (the S-100 feature catalogue and +/// the complex-linestyle table) on the CALLING thread. Both are idempotent lazy-init +/// and thereafter read-only. A host that renders or bakes cells from multiple threads +/// MUST call this once on its main thread before spawning them: then concurrent +/// bake/render is race-free (the allocator is thread-safe, the portrayal context is +/// thread-local, and these two globals are already populated so nobody writes them). +pub fn warmup() void { + catalogue.warmUp(); + var ls_srcs = std.ArrayList(assets.LineStyleSrc).empty; + defer ls_srcs.deinit(gpa); + for (embedded_assets.linestyles) |e| ls_srcs.append(gpa, .{ .id = e.name, .xml = e.bytes }) catch {}; + scene.registerLinestylesXml(gpa, ls_srcs.items); +} + // Parallel open worker: peek each cell's band + bbox and copy its bytes. const OpenWork = struct { inputs: []const CellInput, From 18f5f9f3d7805d251a3c071dc1733f6afc911c13 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 7 Jul 2026 20:43:09 -0400 Subject: [PATCH 17/19] chart: tile57_bake_cell_bytes for persisting a per-cell tile cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bake ONE cell (+ updates, read from disk) to PMTiles bytes in [minzoom, maxzoom], returned for the host to write to a disk cache and reopen with tile57_chart_open_pmtiles — so the slow single-cell bake becomes one-time. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/tile57.h | 7 +++++++ src/capi.zig | 14 ++++++++++++++ src/chart.zig | 10 ++++++++++ 3 files changed, 31 insertions(+) diff --git a/include/tile57.h b/include/tile57.h index 15de1aa..eb21460 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -93,6 +93,13 @@ tile57_chart *tile57_chart_open_header(const char *path); * (progressive load). Renders via the fast reader path. NULL/failure -> NULL. */ tile57_chart *tile57_chart_open_zoom(const char *path, uint8_t minzoom, uint8_t maxzoom); +/* Bake ONE cell (+ its .001.. updates, read from disk) to PMTiles bytes in + * [minzoom, maxzoom], returned in *out / *out_len (free with tile57_free). For a host + * to persist a per-cell tile cache to disk so the slow bake is one-time — then reopen + * the written file with tile57_chart_open_pmtiles. 1=ok, 0=nothing baked, -1=error. */ +int tile57_bake_cell_bytes(const char *path, uint8_t minzoom, uint8_t maxzoom, + uint8_t **out, size_t *out_len); + /* Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked streaming * chart: enumerates cells + peeks each bbox/scale, reads bytes on demand (working * set only). For tile fetch / bake, NOT live render_surface_cb. NULL/failure -> NULL. */ diff --git a/src/capi.zig b/src/capi.zig index 2e3c8a3..a05f19a 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -93,6 +93,20 @@ export fn tile57_chart_open_zoom(path: ?[*:0]const u8, minzoom: u8, maxzoom: u8) return chart.openCellBaked(p, null, true, minzoom, maxzoom) catch null; } +/// Bake ONE cell (+ its updates, read from disk) to PMTiles bytes in [minzoom, +/// maxzoom], into *out / *out_len (free with tile57_free). For persisting a per-cell +/// tile cache to disk. 1=ok, 0=nothing baked, -1=error. See tile57.h. +export fn tile57_bake_cell_bytes(path: ?[*:0]const u8, minzoom: u8, maxzoom: u8, out: *[*]u8, out_len: *usize) callconv(.c) c_int { + const p = spanOpt(path) orelse return -1; + const archive = chart.bakeCellBytes(p, null, minzoom, maxzoom) catch return -1; + if (archive) |a| { + out.* = a.ptr; + out_len.* = a.len; + return 1; + } + return 0; +} + /// Open a whole ENC_ROOT directory (or a single cell) as a lazily-baked chart — the /// `.cells` backend, for tile fetch / bake, not live render_surface_cb. See tile57.h. export fn tile57_charts_open(path: ?[*:0]const u8) callconv(.c) ?*Chart { diff --git a/src/chart.zig b/src/chart.zig index de3c82d..9c0575e 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -591,6 +591,16 @@ pub fn openCellPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) return openCellBaked(path, rules_dir, pick_attrs, 0, 18); } +/// Bake a SINGLE .000 cell (+ updates) to a PMTiles archive in [minzoom, maxzoom] and +/// return the bytes (gpa-owned; free with tile57_free). For a host to persist a +/// per-cell tile cache to disk so the (slow) bake is one-time. null = nothing baked. +pub fn bakeCellBytes(cell_path: []const u8, rules_dir: ?[]const u8, minzoom: u8, maxzoom: u8) !?[]u8 { + var cf = try readCellFiles(cell_path); + defer cf.deinit(); + const cell_in = [_]CellInput{.{ .base = cf.base, .updates = cf.updates }}; + return bakeArchive(&cell_in, resolveRulesDir(rules_dir), minzoom, maxzoom, .mlt, true, null, null); +} + /// Populate the process-global READ-ONLY registries (the S-100 feature catalogue and /// the complex-linestyle table) on the CALLING thread. Both are idempotent lazy-init /// and thereafter read-only. A host that renders or bakes cells from multiple threads From 9d1e8855f2f19b440a7b5c77798bbc95eecb4a33 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Wed, 8 Jul 2026 05:04:20 -0400 Subject: [PATCH 18/19] render: tessellate complex linestyles at render time, display-scaled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complex (symbolised) linestyles were tessellated at BAKE time in native 72-dpi px (period/dash/brick offsets never multiplied by size_scale) and frozen into tiles, while at render the bricks were redrawn display-scaled (refDev) — so on a HiDPI display you got display-sized bricks crammed at native spacing ("too condensed"). Move the period-walk to render time so spacing AND brick size both scale with the display, keeping baked tiles display-independent (the host's disk cache survives a display change): - surface.zig: two optional vtable methods (default null) — size_scale() (a render surface's physical multiplier) and store_complex_run() (bake encoder only). - scene.zig: split emitComplexLine into stage A (clip into phased runs, unchanged) and walkComplexRun (stage B: the period walk, now scaled by the surface's size_scale, with the [0,EXTENT) symbol seam-ownership preserved). At bake each clipped run is STORED un-tessellated in the lines layer tagged ls_style/ls_arc0; on a render surface it is walked live. replayTile re-walks stored runs at the render surface's size_scale (arc0 keeps dash phase continuous across tile seams). - vector.zig / pixel.zig: .line bricks draw at refDev (match the render-scaled period); both surfaces expose size_scale so VectorSurface (OpenCPN) and the png/pdf CLI (--scale) render complex lines consistently. Other Surface impls leave the new fields null and keep tessellating natively — no regression, and it's a no-op at size_scale=1 by construction. Note: the MVT/JS web client renders the baked lines layer directly and does not re-tessellate ls_style runs (draws them plain); the Zig replay path is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/chart.zig | 8 +-- src/render/pixel.zig | 9 +++ src/render/surface.zig | 26 +++++++ src/render/vector.zig | 19 ++++- src/scene/scene.zig | 156 +++++++++++++++++++++++++++++------------ tools/render.zig | 2 +- 6 files changed, 167 insertions(+), 53 deletions(-) diff --git a/src/chart.zig b/src/chart.zig index 9c0575e..1f53a32 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -1108,7 +1108,7 @@ pub const Chart = struct { else @import("tiles").mvt.decode(a, bytes) catch continue; ps.setOrigin(t.origin_x, t.origin_y); - scene.replayTile(surf, layers) catch return error.TileGen; + scene.replayTile(a, surf, layers) catch return error.TileGen; } return surf.endScene(gpa) catch error.TileGen; }, @@ -1176,7 +1176,7 @@ pub const Chart = struct { else @import("tiles").mvt.decode(a, bytes) catch continue; vs.setTile(t.z, t.x, t.y); - scene.replayTile(surf, layers) catch continue; + scene.replayTile(a, surf, layers) catch continue; } }, .cell => |*cb2| { @@ -1236,7 +1236,7 @@ pub const Chart = struct { @import("tiles").mlt.decode(a, bytes) catch return else @import("tiles").mvt.decode(a, bytes) catch return; - scene.replayTile(surf, layers) catch return; + scene.replayTile(a, surf, layers) catch return; }, .cell => |*cb2| { const one = [_]scene.CellRef{.{ @@ -1285,7 +1285,7 @@ pub const Chart = struct { else @import("tiles").mvt.decode(a, bytes) catch continue; as.setOrigin(t.origin_x, t.origin_y); - scene.replayTile(surf, layers) catch return error.TileGen; + scene.replayTile(a, surf, layers) catch return error.TileGen; } return surf.endScene(gpa) catch error.TileGen; }, diff --git a/src/render/pixel.zig b/src/render/pixel.zig index e21bec4..a479c72 100644 --- a/src/render/pixel.zig +++ b/src/render/pixel.zig @@ -132,6 +132,7 @@ pub const PixelSurface = struct { .drawText = drawText, .endFeature = endFeature, .endScene = endScene, + .size_scale = sizeScale, }; /// `a` should be the same scratch arena the engine allocates geometry @@ -225,6 +226,14 @@ pub const PixelSurface = struct { return self.settings.size_scale * @as(f64, @floatCast(self.px_per_tile)) / 256.0; } + /// Surface contract: the physical-size multiplier the engine uses to walk + /// complex-linestyle periods (scene.walkComplexRun), so --scale widens the + /// spacing AND enlarges the bricks together (matching drawSymbol's devScale). + /// NOT devScale: the period is in tile-coord and gets px_per_tile separately. + fn sizeScale(ctx: *anyopaque) f64 { + return sp(ctx).settings.size_scale; + } + fn fillPattern(ctx: *anyopaque, name: rs.SymbolName, rings: []const []const rs.TilePoint) anyerror!void { const self = sp(ctx); const store = self.store orelse return; diff --git a/src/render/surface.zig b/src/render/surface.zig index 8a1a4c9..0f03235 100644 --- a/src/render/surface.zig +++ b/src/render/surface.zig @@ -136,6 +136,19 @@ pub const Surface = struct { endFeature: *const fn (*anyopaque) anyerror!void, /// Finalize the scene; returns encoded bytes owned by `out`. endScene: *const fn (*anyopaque, out: Allocator) anyerror![]u8, + + // ---- Optional (appended; default null so existing vtable literals compile) ---- + + /// A RENDER surface's display scale (settings.size_scale). Present on + /// pixel/vector output surfaces so the engine can walk complex-linestyle + /// periods display-scaled at render time. Null (=> 1.0) on the bake encoder. + size_scale: ?*const fn (*anyopaque) f64 = null, + /// Present ONLY on the bake encoder: store an un-tessellated clipped + /// complex-linestyle run (tile-local integer points) + the style id so + /// replay can re-lookup its LsInfo and re-walk the period at render time. + /// The baked tile stays display-independent (the disk cache survives a + /// display change). Null on render surfaces (they walk, not store). + store_complex_run: ?*const fn (*anyopaque, style: []const u8, color: ColorToken, width_px: f64, arc0: f64, run: []const TilePoint) anyerror!void = null, }; pub fn beginScene(self: Surface, z: u8) anyerror!void { @@ -168,4 +181,17 @@ pub const Surface = struct { pub fn endScene(self: Surface, out: Allocator) anyerror![]u8 { return self.vtable.endScene(self.ptr, out); } + + /// This render surface's display scale (1.0 when unset — the bake encoder). + pub fn sizeScale(self: Surface) f64 { + return if (self.vtable.size_scale) |f| f(self.ptr) else 1.0; + } + /// True on the bake encoder: complex runs are stored (not walked) here. + pub fn canStoreComplexRun(self: Surface) bool { + return self.vtable.store_complex_run != null; + } + /// Store one clipped, un-tessellated complex-linestyle run (bake path only). + pub fn storeComplexRun(self: Surface, style: []const u8, color: ColorToken, width_px: f64, arc0: f64, run: []const TilePoint) anyerror!void { + return self.vtable.store_complex_run.?(self.ptr, style, color, width_px, arc0, run); + } }; diff --git a/src/render/vector.zig b/src/render/vector.zig index 76c260d..3849dbf 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -170,6 +170,9 @@ pub const VectorSurface = struct { .drawText = drawText, .endFeature = endFeature, .endScene = endScene, + // Render surface: the engine walks complex-linestyle periods at this scale. + // (No store_complex_run — this surface WALKS/renders runs, never stores.) + .size_scale = sizeScale, }; pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, cb: *const CSurface) VectorSurface { @@ -212,6 +215,13 @@ pub const VectorSurface = struct { return self.settings.size_scale; } + /// Surface contract: this render surface's display scale (settings.size_scale). + /// The engine reads it to walk complex-linestyle periods display-scaled so a + /// HiDPI display gets both wider spacing AND bigger bricks (baked tiles native). + fn sizeScale(ctx: *anyopaque) f64 { + return sp(ctx).refDev(); + } + /// Tile-space point -> web-mercator world [0,1] (y down). fn worldOf(self: *const VectorSurface, p: rs.TilePoint) CWorldPt { const n = std.math.pow(f64, 2.0, @floatFromInt(self.tz)); @@ -319,6 +329,7 @@ pub const VectorSurface = struct { fn drawSymbol(ctx: *anyopaque, name: rs.SymbolName, at: rs.TilePoint, rot_deg: f64, scale: f64, rot_north: bool, placement: rs.SymbolPlacement, danger_depth: ?f64) anyerror!void { _ = rot_north; + _ = placement; // both .point and .line now draw at display size (refDev) const self = sp(ctx); const store = self.store orelse return; if (!resolve.visible(&self.cur, name, GATE_ZOOM, self.settings)) return; @@ -331,9 +342,11 @@ pub const VectorSurface = struct { const s = store.get(eff) orelse return; // Draw as an atlas sprite when the host supports it; else tessellate. // Symbols never participate in declutter (S-52 icon-allow-overlap — they - // all draw). A .point navaid draws at display size (refDev); a .line - // brick draws at NATIVE size (dev 1.0) so it tiles at the engine spacing. - const dev: f64 = if (placement == .point) self.refDev() else 1.0; + // all draw). Both .point navaids AND .line bricks draw at display size + // (refDev): the complex-linestyle period is now walked at render time + // scaled by size_scale (scene.walkComplexRun), so the brick must scale to + // match — otherwise bricks would be native-sized at display-scaled spacing. + const dev: f64 = self.refDev(); if (self.cb.draw_sprite) |ds| self.emitSprite(ds, eff, s, at, rot_deg, scale, dev, null) else try self.emitSymbol(s, at, rot_deg, scale); } diff --git a/src/scene/scene.zig b/src/scene/scene.zig index 851b9a7..d315215 100644 --- a/src/scene/scene.zig +++ b/src/scene/scene.zig @@ -674,6 +674,9 @@ pub const TileSurface = struct { .drawText = drawText, .endFeature = endFeature, .endScene = endScene, + // Bake path: store complex runs un-tessellated (no size_scale set — bake is + // native; replay re-walks the period display-scaled). + .store_complex_run = storeComplexRun, }; pub fn init(a: Allocator, format: TileFormat) TileSurface { @@ -769,6 +772,22 @@ pub const TileSurface = struct { try s.linesL().append(s.a, .{ .geom_type = .linestring, .parts = lines, .properties = props.items }); } + /// Store one clipped complex-linestyle run un-tessellated (display-independent + /// bake): a `lines` feature tagged with `ls_style`/`ls_arc0` so replayTile + /// re-looks-up the LsInfo and re-walks the period display-scaled at render time. + fn storeComplexRun(ctx: *anyopaque, style: []const u8, color: rs.ColorToken, width_px: f64, arc0: f64, run: []const rs.TilePoint) anyerror!void { + const s = sp(ctx); + var props = std.ArrayList(mvt.Prop).empty; + try props.append(s.a, .{ .key = "color_token", .value = .{ .string = color } }); + try props.append(s.a, .{ .key = "width_px", .value = .{ .double = width_px } }); + try props.append(s.a, .{ .key = "ls_style", .value = .{ .string = style } }); + try props.append(s.a, .{ .key = "ls_arc0", .value = .{ .double = arc0 } }); + try appendMeta(s.a, &props, s.cur); + const parts = try s.a.alloc([]const mvt.Point, 1); + parts[0] = run; + try s.linesL().append(s.a, .{ .geom_type = .linestring, .parts = parts, .properties = props.items }); + } + fn drawSymbol(ctx: *anyopaque, name: rs.SymbolName, at: rs.TilePoint, rot_deg: f64, scale: f64, rot_north: bool, placement: rs.SymbolPlacement, danger_depth: ?f64) anyerror!void { const s = sp(ctx); var props = std.ArrayList(mvt.Prop).empty; @@ -1769,7 +1788,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, if (!std.mem.eql(u8, ln.style, "solid")) { if (g_linestyles.get(ln.style)) |info| { // Complex linestyle: tessellated dash runs + tangent-rotated symbols. - try emitComplexLine(a, stroke_geo, info, ln.color, !opts.suppress_points, z, x, y, box, &fmeta, surf); + try emitComplexLine(a, stroke_geo, info, ln.style, ln.color, !opts.suppress_points, z, x, y, box, &fmeta, surf); continue; } } @@ -2238,11 +2257,68 @@ fn lsSubPathByArc(a: Allocator, rp: []const tile.FPoint, rarc: []const f64, d0_i /// Tessellate a complex linestyle along a feature's geometry parts into this tile. /// `emit_symbols` is false when best-band suppression drops the coarse cell's points. -fn emitComplexLine(a: Allocator, parts: []const []s57.LonLat, info: LsInfo, color: []const u8, emit_symbols: bool, z: u8, x: u32, y: u32, box: tile.Box, fmeta: *const rs.FeatureMeta, surf: rs.Surface) !void { +/// Stage B: walk ONE clipped tile-local run by the complex-linestyle period, +/// emitting dash "on" segments and tangent-rotated embedded symbols. Runs are +/// already tile-local (no z/x/y needed). At RENDER the period AND the px offsets +/// are multiplied by `size_scale`, so spacing and brick size both scale with the +/// display (vector.zig scales the brick to match) and the BAKED tiles stay +/// display-independent. `size_scale <= 0` (bake / CLI) collapses to 1.0. +fn walkComplexRun(a: Allocator, rp: []const tile.FPoint, arc0: f64, info: LsInfo, color: []const u8, size_scale: f64, emit_symbols: bool, surf: rs.Surface) !void { + if (rp.len < 2) return; const ext: f64 = @floatFromInt(tile.EXTENT); const px_scale = ext / 256.0; // figures are laid out in 256-px-per-tile space - const period = info.period_px * px_scale; + const ss = if (size_scale > 0) size_scale else 1.0; + const period = info.period_px * px_scale * ss; if (period < 1e-6) return; + const rarc = try a.alloc(f64, rp.len); + rarc[0] = 0; + for (1..rp.len) |i| rarc[i] = rarc[i - 1] + std.math.hypot(rp[i].x - rp[i - 1].x, rp[i].y - rp[i - 1].y); + const g0 = arc0; + const run_end = g0 + rarc[rp.len - 1]; + var k: i64 = @intFromFloat(@floor(g0 / period)); + while (@as(f64, @floatFromInt(k)) * period < run_end) : (k += 1) { + const base = @as(f64, @floatFromInt(k)) * period; + for (info.on_runs) |on| { // dash on-runs -> line segments + const lo = @max(base + on[0] * px_scale * ss, g0); + const hi = @min(base + on[1] * px_scale * ss, run_end); + if (hi - lo < 1e-6) continue; + const sub = try lsSubPathByArc(a, rp, rarc, lo - g0, hi - g0); + if (sub.len < 2) continue; + const seg = try a.alloc(mvt.Point, sub.len); + for (sub, 0..) |spt, i| seg[i] = tile.quantizeF(spt); + const segparts = try a.alloc([]const mvt.Point, 1); + segparts[0] = seg; + try surf.strokeLine(color, info.width_px, .solid, segparts, null); + } + if (!emit_symbols) continue; + for (info.symbols) |sym| { // embedded symbols -> tangent-rotated points + if (sym.name.len == 0) continue; + const gp = base + sym.offset_px * px_scale * ss; + if (gp < g0 or gp > run_end) continue; + const tp = lsPointAndTangent(rp, rarc, gp - g0) orelse continue; + const qp = tile.quantizeF(tp.p); + // Own each embedded symbol by exactly ONE tile: emit it only when its + // position lands inside the RAW tile [0,EXTENT). The dash-run lines + // keep the buffered clip (seamless strokes across the seam), but a + // symbol in the buffer zone would otherwise be tessellated by BOTH + // this tile and its neighbour -> the same symbol drawn twice at every + // tile seam (user-reported "double symbols"). Half-open so a symbol on + // the seam belongs to exactly one side (no gap, no double). + if (qp.x < 0 or qp.x >= tile.EXTENT or qp.y < 0 or qp.y >= tile.EXTENT) continue; + const rot = std.math.atan2(tp.dy, tp.dx) * 180.0 / std.math.pi; + try surf.drawSymbol(sym.name, qp, rot, SYMBOL_SCALE, true, .line, null); + } + } +} + +/// Tessellate a complex linestyle along a feature's geometry parts into this tile. +/// `emit_symbols` is false when best-band suppression drops the coarse cell's points. +/// `style` is the linestyle id: at BAKE we store each clipped run un-tessellated +/// (tagged with the id) so replay can re-walk the period display-scaled; on a live +/// render surface we walk it here at that surface's size_scale. +fn emitComplexLine(a: Allocator, parts: []const []s57.LonLat, info: LsInfo, style: []const u8, color: []const u8, emit_symbols: bool, z: u8, x: u32, y: u32, box: tile.Box, fmeta: *const rs.FeatureMeta, surf: rs.Surface) !void { + const store = surf.canStoreComplexRun(); + const ss = surf.sizeScale(); try surf.beginFeature(fmeta); for (parts) |part| { if (part.len < 2) continue; @@ -2252,46 +2328,15 @@ fn emitComplexLine(a: Allocator, parts: []const []s57.LonLat, info: LsInfo, colo arc[0] = 0; for (1..part.len) |i| arc[i] = arc[i - 1] + std.math.hypot(fpts[i].x - fpts[i - 1].x, fpts[i].y - fpts[i - 1].y); for (try tile.clipLinePhased(a, fpts, arc, box)) |run| { - const rp = run.points; - if (rp.len < 2) continue; - const rarc = try a.alloc(f64, rp.len); - rarc[0] = 0; - for (1..rp.len) |i| rarc[i] = rarc[i - 1] + std.math.hypot(rp[i].x - rp[i - 1].x, rp[i].y - rp[i - 1].y); - const g0 = run.arc0; - const run_end = g0 + rarc[rp.len - 1]; - var k: i64 = @intFromFloat(@floor(g0 / period)); - while (@as(f64, @floatFromInt(k)) * period < run_end) : (k += 1) { - const base = @as(f64, @floatFromInt(k)) * period; - for (info.on_runs) |on| { // dash on-runs -> line segments - const lo = @max(base + on[0] * px_scale, g0); - const hi = @min(base + on[1] * px_scale, run_end); - if (hi - lo < 1e-6) continue; - const sub = try lsSubPathByArc(a, rp, rarc, lo - g0, hi - g0); - if (sub.len < 2) continue; - const seg = try a.alloc(mvt.Point, sub.len); - for (sub, 0..) |spt, i| seg[i] = tile.quantizeF(spt); - const segparts = try a.alloc([]const mvt.Point, 1); - segparts[0] = seg; - try surf.strokeLine(color, info.width_px, .solid, segparts, null); - } - if (!emit_symbols) continue; - for (info.symbols) |sym| { // embedded symbols -> tangent-rotated points - if (sym.name.len == 0) continue; - const gp = base + sym.offset_px * px_scale; - if (gp < g0 or gp > run_end) continue; - const tp = lsPointAndTangent(rp, rarc, gp - g0) orelse continue; - const qp = tile.quantizeF(tp.p); - // Own each embedded symbol by exactly ONE tile: emit it only when its - // position lands inside the RAW tile [0,EXTENT). The dash-run lines - // keep the buffered clip (seamless strokes across the seam), but a - // symbol in the buffer zone would otherwise be tessellated by BOTH - // this tile and its neighbour -> the same symbol drawn twice at every - // tile seam (user-reported "double symbols"). Half-open so a symbol on - // the seam belongs to exactly one side (no gap, no double). - if (qp.x < 0 or qp.x >= tile.EXTENT or qp.y < 0 or qp.y >= tile.EXTENT) continue; - const rot = std.math.atan2(tp.dy, tp.dx) * 180.0 / std.math.pi; - try surf.drawSymbol(sym.name, qp, rot, SYMBOL_SCALE, true, .line, null); - } + if (run.points.len < 2) continue; + if (store) { + // BAKE: store the clipped run un-tessellated (display-independent). + const qpts = try a.alloc(mvt.Point, run.points.len); + for (run.points, 0..) |p, i| qpts[i] = tile.quantizeF(p); + try surf.storeComplexRun(style, color, info.width_px, run.arc0, qpts); + } else { + // LIVE / CLI render: walk the period at this surface's display scale. + try walkComplexRun(a, run.points, run.arc0, info, color, ss, emit_symbols, surf); } } } @@ -2381,7 +2426,7 @@ fn emitNavSystemFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize // Tessellate the registered complex linestyle (dashes + the A/B letter symbols). if (g_linestyles.get(boundary)) |info| { - try emitComplexLine(a, nav_parts, info, "CHGRD", !opts.suppress_points, z, x, y, box, &fmeta, surf); + try emitComplexLine(a, nav_parts, info, boundary, "CHGRD", !opts.suppress_points, z, x, y, box, &fmeta, surf); return; } // No registered linestyle (live/host path, no table): a plain dashed CHGRD ring. @@ -3579,7 +3624,7 @@ fn metaFromProps(props: []const mvt.Prop) rs.FeatureMeta { /// Replay one decoded tile's layers as Surface calls (between the caller's /// begin/endScene). Layer names route exactly as TileSurface emitted them. -pub fn replayTile(surf: rs.Surface, layers: []const mvt.DecodedLayer) !void { +pub fn replayTile(a: Allocator, surf: rs.Surface, layers: []const mvt.DecodedLayer) !void { for (layers) |layer| { const is_areas = std.mem.startsWith(u8, layer.name, "areas"); const is_patterns = std.mem.startsWith(u8, layer.name, "area_patterns"); @@ -3598,6 +3643,27 @@ pub fn replayTile(surf: rs.Surface, layers: []const mvt.DecodedLayer) !void { const dr: ?rs.DepthRange = if (d1) |v| .{ .d1 = @floatCast(v), .d2 = @floatCast(propF64(f.properties, "drval2") orelse v) } else null; try surf.fillArea(propStr(f.properties, "color_token"), f.parts, dr); } else if (is_lines) { + // Complex (symbolised) linestyle: the bake stored the clipped run + // un-tessellated (tagged ls_style). Re-walk the period at the render + // surface's display scale so spacing + brick size track the display. + const ls_style = propStr(f.properties, "ls_style"); + if (ls_style.len > 0) { + if (g_linestyles.get(ls_style)) |info| { + const color = propStr(f.properties, "color_token"); + const arc0 = propF64(f.properties, "ls_arc0") orelse 0; + for (f.parts) |part| { + if (part.len < 2) continue; + const fpts = try a.alloc(tile.FPoint, part.len); + for (part, 0..) |p, i| fpts[i] = .{ .x = @floatFromInt(p.x), .y = @floatFromInt(p.y) }; + try walkComplexRun(a, fpts, arc0, info, color, surf.sizeScale(), true, surf); + } + continue; + } + // Style not registered (host without the table): fall back to a + // plain dashed stroke so the line never disappears. + try surf.strokeLine(propStr(f.properties, "color_token"), propF64(f.properties, "width_px") orelse 1, .dashed, f.parts, null); + continue; + } const dash: rs.Dash = if (std.mem.eql(u8, propStr(f.properties, "dash"), "solid")) .solid else .dashed; try surf.strokeLine(propStr(f.properties, "color_token"), propF64(f.properties, "width_px") orelse 1, dash, f.parts, propF64(f.properties, "valdco")); } else if (is_points) { diff --git a/tools/render.zig b/tools/render.zig index f615d0d..8926fcf 100644 --- a/tools/render.zig +++ b/tools/render.zig @@ -204,7 +204,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: else engine.mvt.decode(a, tb) catch continue; ps.setOrigin(t.origin_x, t.origin_y); - try engine.scene.replayTile(surf, layers); + try engine.scene.replayTile(a, surf, layers); } break :blk try surf.endScene(a); } else blk: { From 03c49a6920acad1b6a01115abb6081d82eafbc2c Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Wed, 8 Jul 2026 06:02:40 -0400 Subject: [PATCH 19/19] render: declutter soundings (per-number) so HiDPI enlargement doesn't crowd them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Soundings are S-52 icon-allow-overlap and never decluttered, but the ×size_scale enlargement on a HiDPI display crowds them into an unreadable mass. Give each sounding NUMBER a single declutter box (the union of its digit glyphs' extents) placed once in the VectorSurface's screen-px declutter grid: keep the first-emitted (higher priority) number, drop later numbers it overlaps. The digits of a kept number still draw ungated so the number stays intact. VectorSurface-only (the OpenCPN render path); the raster CLI PixelSurface is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/render/vector.zig | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/render/vector.zig b/src/render/vector.zig index 3849dbf..25ec185 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -388,8 +388,32 @@ pub const VectorSurface = struct { const shown = if (feet) depth_m * sndfrm.M_TO_FT else depth_m; const prefix: []const u8 = if (depth_m <= self.settings.safety_depth) "SOUNDS" else "SOUNDG"; const list = try sndfrm.syms(self.a, prefix, shown, swept, low_acc, feet); - // Soundings are symbols (S-52 icon-allow-overlap): they do NOT declutter; - // density is governed by SCAMIN. Each digit self-positions by its pivot. + // S-52 lets soundings overlap (icon-allow-overlap), but the HiDPI ×size_scale + // enlargement crowds them into an unreadable mass. Thin them: declutter each + // NUMBER as one box (the union of its digit glyphs), placed once — keep the + // first-emitted (higher draw-priority) and drop later numbers it overlaps. The + // digits themselves still draw ungated (below) so the kept number stays intact. + const k_decl = sndfrm.SYMBOL_SCALE * 100.0 * self.refDev(); + var uw: f64 = 0; + var uh: f64 = 0; + { + var itb = std.mem.splitScalar(u8, list, ','); + while (itb.next()) |glyph| { + if (glyph.len == 0) continue; + const s = store.get(glyph) orelse continue; + for (s.paths) |p| for (p.contours) |contour| for (contour) |c| { + const lx = @abs((c.x - s.pivot.x) * k_decl); + const ly = @abs((c.y - s.pivot.y) * k_decl); + if (lx > uw) uw = lx; + if (ly > uh) uh = ly; + }; + } + } + if (uw > 0 and uh > 0) { + const scale_px = 256.0 * std.math.exp2(self.view_zoom); + const anchor = self.worldOf(at); + if (!self.declutter.place(self.a, anchor.x * scale_px, anchor.y * scale_px, uw, uh, false)) return; + } var it = std.mem.splitScalar(u8, list, ','); while (it.next()) |glyph| { if (glyph.len == 0) continue;