feat(capi): callbacks C embedders can render primitives#2
Merged
Conversation
…selves 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) <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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) <noreply@anthropic.com>
029da06 to
ff9fe3b
Compare
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
… crowd them 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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…selves
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.
Geometry is emitted in canvas pixel space (y down), in final paint order, with colours resolved for the active palette.