diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f0aff5f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + # Zig package deps (build.zig.zon) aren't supported by Dependabot; the submodules + # carry the vendored catalogues. So the actionable surface here is the CI actions — + # keep them patched. + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0007b10..1081d88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: uses: mlugg/setup-zig@v2 with: version: 0.16.0 + - name: Format check + run: zig fmt --check src/ tools/ build.zig - name: Build (native) run: zig build -Doptimize=ReleaseFast - name: Test diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7753891..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,375 +0,0 @@ -# Changelog - -All notable changes to chartplotter-native. The format loosely follows -[Keep a Changelog](https://keepachangelog.com/); the project is pre-1.0 and the -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) - data-coverage polygon, clipped per tile, into the `area_patterns` source - layer as pattern `OVERSC01`, tagged with a new int prop `oscl` = the cell's - compilation scale quantized UP the scamin ladder (same crossing alignment as - the `smax` handoff). Area fills carry the same `oscl` tag. -- **The occlusion sandwich** (style builder): the base `areas` fill layer - splits into `fill-areas#oscl` (fills of overscaled cells) UNDER the new - `overscale` layer (fill-pattern `pat:OVERSC01`, source-layer - `area_patterns`) UNDER `fill-areas` (at-scale fills) — a finer cell's opaque - DEPARE/LNDARE occlude a coarser cell's hatch, so the hatch survives only on - coarse-only patches, with zero polygon boolean ops. The generic pattern - layers exclude `OVERSC01`. -- **The oscl gate clause** (client-injected, exact shape pinned by test): - `[">", ["coalesce",["get","oscl"], 0], DENOM]` on the hatch + under-hatch - fills, and its `["!", …]` negation on the at-scale fills. DENOM is the SAME - injected literal as the scamin/smax clauses (filter-gate mode), a - zoom-derived denominator in bucket/fallback modes, and the 1e12 show-all - placeholder at boot (hatch hidden, all fills at-scale, until the client - injects the live denom). -- **`show_overscale` mariner toggle** (default true; S-52 §10.1.10): drives the - `overscale` layer's `layout.visibility` only, so a toggle is a single - `setLayoutProperty` style-diff op. C ABI: `tile57_mariner.show_overscale` - (appended; `tile57_mariner_defaults` sets true); bindings/go: - `Mariner.ShowOverscale`. -- **Pixel/ascii surfaces**: `resolve.osclVisible` mirrors the style gate, so - render_view/pdf/ascii draw the hatch consistently (via the normal AP path); - hidden under `ignore_scamin` and when `show_overscale` is off. -- **Manifest ladder**: emitted `oscl` values join the archive/live `scamin` - ladder (bundle prop scan + per-cell quantized-cscl fold), so the client's - discrete crossing machinery fires exactly at every emitted gate value. - -### Changed — MLT is the default tile format -- **MLT (MapLibre Tiles) is now the DEFAULT bake/storage tile format**; - MVT stays available explicitly (`--format mvt`, `tile57_bake_opts.format = - TILE57_TILE_TYPE_MVT`). There is NO transcode layer anywhere: the wire format - follows the storage format, and maplibre-gl ≥ 5.12 decodes MLT natively via - the vector-source `encoding` option. -- **Bake parity gap closed**: the bundle's post-bake collection (sprite-mln - sounding composites + the SCAMIN manifest feeding the client's ladder) now - decodes baked tiles with the codec matching the bake format, so an MLT bake - produces byte-identical sprite-mln output and the same `scamin` metadata as - an MVT bake of the same input. -- C ABI: `tile57_bake_opts` gained `format` (0 = default = MLT; honored by - `tile57_bake_pmtiles` AND `tile57_bake_bundle`); `tile57_chart_info` gained - `tile_type` (the encoding `tile57_chart_tile` returns — the stored type for a - PMTiles chart, the live generation format for a cell chart); new - `tile57_chart_set_tile_format` selects the live-generation encoding on a - cell-backed chart (cell charts still OPEN generating MVT so existing MVT-only - embedders are unaffected); `tile57_style_template` gained `tile_encoding` - (emits `"encoding":"mlt"` on the chart source; the hint survives - `tile57_build_style` / `tile57_style_diff`); the stale "decompressed MVT - bytes" doc on `tile57_chart_tile` now states tiles serve verbatim in the - chart's tile encoding. Bundle styles (`style-{day,dusk,night}.json`) carry - `"encoding":"mlt"` on their `pmtiles://` source for MLT bundles. -- bindings/go: `BakeOpts.Format`, `ChartInfo.TileType` / `Meta.TileType`, - `Source.SetTileFormat`, `TileFormat` (+ `Encoding()`/`EncodingFormat`), and an - `encoding TileFormat` parameter on `StyleTemplate`/`Style`. -- `tile57 inspect` now decodes MLT tiles (same layer/feature dump as MVT) - instead of hex-dumping them. - -### Fixed — scamin-aware band handoff (the band-floor blank window) -- **Zooming through a band boundary no longer blanks the chart** while the finer - band's SCAMIN-gated bulk is still hidden. Each band's floor-zoom tiles now bake - in the next-coarser band's pass with BOTH bands' cells alive; where a tile's - display window opens coarser than the covering finer cell's compilation scale, - the coarser band's features are carried down tagged with a new integer tile - property **`smax`** (the handoff denominator, quantized up onto the archive's - real SCAMIN ladder) instead of being best-band suppressed. Styles gate it with - `["<", ["coalesce",["get","smax"],0], DENOM]` beside the scamin clause (the - same injected literal in filter-gate mode; a zoom-derived denominator on the - bucket/fallback paths), so the copy hands off at the exact crossing where the - finer content activates — no hole, no double-draw. The live cells backend - applies the same rule per request (`bake_enc.carryGate`, shared). -- **Bundle bakes no longer lose every zoom below the coarsest populated band's - floor.** The coarsest band with cells now extends down to the archive minzoom - (mirroring the live path's coarsest-band fallback), so a pack without overview - cells still gets low-zoom tiles instead of blank basemap. -- Bake callers hold at most **two adjacent bands'** cells at once (the deferred - band rides into the next pass; portrayal is reused, never recomputed). - `Baker.bakeBand`/`plannedTiles` grew an own/carry split (`own_len`) and a - `FloorMode` (`defer_down`/`extend_min`). - -### Changed — chart-centric C ABI (breaking, pre-1.0) -- **The tile-source C ABI (`include/tile57.h`) was reworked into a chart-centric - surface** (see `docs/docs/c-api.md`); the internal Zig type `Source` in `src/source.zig` - is likewise renamed **`Chart`** in `src/chart.zig`, so the engine and the ABI use - the same word. The opaque handle is `tile57_chart` and the open/serve entry points - are prefixed `tile57_chart_*`. - - **Open** splits the old single `tile57_source_open(data, len, format, rules_dir)` - (and its `TILE57_FORMAT_*` enum) into three explicit functions: - `tile57_chart_open(path)` (an on-disk ENC_ROOT directory or a single `.000`, - streamed on demand), `tile57_chart_open_bytes(base, len)` (one in-memory S-57 - cell), and `tile57_chart_open_pmtiles(path)` (a baked PMTiles bundle). The - many-cells / streaming C openers (`tile57_source_open_cells{,_streaming}`) and - the resolved-format getter `tile57_source_format` are gone. - - **Metadata** — the four `tile57_source_{zoom_range,bands,bounds,anchor}` getters - fold into one `tile57_chart_get_info(chart, &info)` filling a `tile57_chart_info` - struct; a new `tile57_chart_scamin` returns the live SCAMIN manifest. - - **Tiles / lifetime** — `tile57_tile_get` → `tile57_chart_tile`, - `tile57_source_clear_cache` → `tile57_chart_clear_cache`, `tile57_source_close` - → `tile57_chart_close`. The per-kind frees collapse into one universal - `tile57_free(ptr, len)` (replacing `tile57_tile_free`). -- **Bake surface.** `tile57_bake_cells` → `tile57_bake_pmtiles`, now taking a - `tile57_cell` array (renamed from `tile57_cell_input`) plus a `tile57_bake_opts` - options struct (rules/catalog dir, zoom clamp, pick attrs, progress); the - `tile57_bake_progress` callback gains band index/count/name. New - `tile57_bake_bundle(input, out_dir, opts, …)` writes the full on-disk bundle the - `tile57 bake … -o out/` CLI emits. -- **Portrayal assets folded.** The four generators (`tile57_colortables`, - `tile57_linestyles`, `tile57_sprite_atlas`, `tile57_pattern_atlas`) and the - `tile57_named_bytes` blob type are replaced by one `tile57_bake_assets(catalog_dir, - &assets)` filling a `tile57_assets` struct (colortables + linestyles + sprite + - pattern buffers), freed with `tile57_assets_free`. `tile57_colortables_default` / - `tile57_style_template` still build a style with no on-disk catalogue, and - `tile57_build_style` / `tile57_style_diff` take the SCAMIN manifest. - -### Changed — repository split into the tile57 engine + the Qt demo -- **chartplotter-native is now the standalone tile57 engine.** The Zig engine - moved from `engine/` to the repository root, so a top-level **`zig build`** - produces `zig-out/bin/tile57` (CLI) + `zig-out/lib/libtile57.a` (C ABI). A plain - `zig build` now defaults to **ReleaseFast** (a Debug bake is ~2.6x slower); - use `-Doptimize=Debug` for development. -- **Removed the C++ render layer.** The headless renderer (`chartplotter-render` / - `libchartplotter`), its mbgl `FileSource` adapter, the vendored - `maplibre-native` submodule, `include/chartplotter.h`, the root CMake build, and - the PNG-render helper scripts are gone. The repo is now pure Zig - tile/style/asset generation behind the C ABI (`include/tile57.h` + - `tile57_diag.h`). -- **The Qt6 viewer moved to a standalone repo, `tile57-demo`**, which consumes the - tile57 engine as a git submodule (built via `zig build`) and QMapLibre. - -### Changed — interactive window is now Qt6 (QMapLibre) -- **Replaced the GLFW / macOS-Metal MapLibre Native window with a Qt6 viewer**, - `chartplotter-qt` (`app/qt`), built on the QMapLibre widget (maplibre-native-qt, - vendored as a submodule + built by `scripts/build-qmaplibre.sh`). It loads a baked - chart bundle's `style.json` (PMTiles + portrayal assets) into a - `QMapLibre::MapWidget` — no mbgl internals, no custom FileSource. -- **Removed** the GLFW + Metal/native-host window code (`app/chartplotter_main.cpp`, - `chart_native_host`, `metal_backend`, `chart_renderer_frontend`), the - `MLN_WITH_GLFW` CMake path + the `chartplotter` exe, the desktop/macos-desktop - presets, and the `chartplotter_view_*` C API. `libchartplotter` is now the headless - renderer (`chartplotter_render_png` + `chartplotter-render`); ChartTileSource / - enc_root and the offline baker are unchanged. - -### Changed — engine reshaped into foundational packages -- **`iso8211`, `s57`, `s100` are now standalone Zig packages** (`engine/src/{iso8211, - s57,s100}/`), mirroring the Go oracle's `pkg/iso8211`, `pkg/s57`, `pkg/s100` one - for one. They are pure (no libc/Lua) and target-agnostic, so the same modules - compile into both the unit tests (glibc) and the static-musl baker. Pure refactor - — behavior identical; the C ABI (`tile57_*`) is unchanged. - -### Added — offline chart-bundle baker -- **`tile57 bundle -o `** emits a self-contained, - relocatable **chart bundle**: `tiles/chart.pmtiles` + `assets/colortables.json` + - `assets/style-{day,dusk,night}.json` + `manifest.json`. The tiles carry S-52 colour - *tokens* (palette-independent); the assets carry the RGB and the style layers. Both - halves come from the same S-101 catalogue and are stamped with a `schema_version` - (`tile57/1`) so a renderer can refuse a mismatched bundle. -- **New `assets` module** (mirrors the Go oracle's `internal/engine/assets.EmitS101`) - and **`tile57 assets -o `** emit the portrayal - assets independent of a cell. First artifact: `colortables.json` (token → hex per - day/dusk/night palette, parsed from `ColorProfiles/colorProfile.xml`) — **byte- - identical to the Go oracle's output**. -- **MapLibre `style.json` generation** (`assets/style.zig`, a port of - ported from the web `s52-style.mjs`/`chart-style.mjs`): `tile57 style` - emits one style per palette, and `bundle` writes the three styles + references them - in `manifest.portrayal.styles`, so a bundle is **directly renderable**. - -### Removed -- **The legacy Python style generator `style/build_style.py`** (and the - transitional `scripts/check-style-parity.sh`). `engine/src/assets/style.zig` is - now the sole style generator — verified full-file identical to `build_style.py` - (27 layers × 3 palettes) before removal — and `scripts/gen-style.sh` drives - `tile57 style`. Line styles, sprite/pattern atlases (SVG raster), and - glyphs (SDF) — to light up the symbol/text layers — are next. - -### Added — on-demand ENC_ROOT + offline baker -- **Lazy on-demand tile generation (the new default for an ENC_ROOT).** Pointing a - host at an ENC_ROOT used to parse + portray *every* cell at open and hold them all - (≈34 GB for the ~7400-cell NOAA catalogue — a blue, never-finishing window). Now - `tile57_source_open_cells` builds a cheap spatial index (per cell: compilation- - scale band + bbox, via `s57.peekMeta`, in parallel) and parses + portrays only - the cells a *requested tile* needs, choosing the best-available scale band per - tile and keeping recent cells in an LRU. The full catalogue opens in seconds and - renders a view by touching ~a dozen cells (≈5 GB transient at open, far less - steady-state). Coverage gaps at a zoom are filled by overzooming the coarsest - band present, so tiles aren't blank. -- **No band-boundary tile gaps + scale-clamped navigation.** A tile now overlays - *all* overlapping cells whose band range includes the zoom (coarse→fine, finer on - top), so a finer cell that covers only part of a tile no longer blanks the coarser - fill (the earlier best-band single-pick left holes); a zoom in a coverage gap - overzooms the coarsest band. The window clamps navigation to the chart scale range - ~1:10,000,000 .. 1:4,000 (z≈5.8..17.1) and the vector source minzoom drops to 5 - (`gen-style.sh`) so overview scales actually draw. LRU budget raised to 256 cells - for wide views. -- **Initial-view framing.** When no explicit camera is given, the host fits the - data bounds if that lands at a usable zoom (a single cell / a region); for a - continental ENC_ROOT — whose bounds would fit at ~z2, below the style's source - minzoom, i.e. blank — it instead opens on a representative harbour cell near the - data median (`tile57_source_anchor`), robust to scattered IHO test cells. Fixes - "the window comes up but I see nothing." `chartplotter-render` with zoom `0` - exercises the same path. -- **Offline streaming banded baker** (`engine/src/bake_enc.zig`): for precomputing - a shareable PMTiles archive. Groups cells into navigational bands by CSCL (Band, - matching the Go reference), bakes finest → coarsest with best-band dedup, holding - one band at a time. Portrayal + per-tile MVT generation run across all cores - (`parallelFor`; thread-local Lua context, warmed catalogue, quiet flag); per-cell - geometry is assembled once (`buildGeoCache`) and reused. CLI `tile57 - bake-root -o out.pmtiles`; C ABI `tile57_bake_cells`. The app can use - it on open via `CHARTPLOTTER_BAKE=1` (cached under `$XDG_CACHE_HOME/chartplotter`, - `CHARTPLOTTER_BAKE_MIN/MAXZOOM` to set the range) for smooth-everywhere offline - use; the default is lazy. - -### Added — offline baker + portrayal/style refinements -- **Spatial / geometry Host binding**: `HostGetSpatial` now serves real point - geometry (`#P` points, `#M` multipoints, `#S` surfaces) to the S-101 rules via - `_HostFeaturePoints` (backed by per-feature point geometry). A `#P` point must be - a real Point or the framework's `GetSpatial` recurses forever — this was the - Obstruction/Wreck "C stack overflow". With it + derived depths + the - orientation/clearance complex attrs below, the test cells (US4MD81M, US5MD1MC) - now portray with **zero rule errors**. -- **Orientation + clearance complex attributes** synthesized from S-57 simple - attrs (ORIENT, VERCCL/VERCLR/VERCOP, HORCLR) so NavigationLine / RecommendedTrack - / SpanOpening read `feature..` instead of crashing on a nil - complex (Go sync: the `clearances` map + orientation alias). -- **`tile57` CLI** (`engine/tools/bake.zig`): pre-bake a cell to a - PMTiles archive — `tile57 bake -o out.pmtiles - [--rules DIR] [--minzoom N --maxzoom N] [updates…]` — over the cell's bounds, - plus `inspect`, `cell`, and `version`. The precache path that mirrors - chartplotter-go's `bake`. -- **Full S-101 portrayal in the baker.** `tile57` now runs the same - embedded-Lua S-101 rule engine as the live library (not the `classify()` - fallback), so baked tiles carry the full S-101 layer set - (areas/area_patterns/lines/point_symbols/soundings/text + `*_scamin` declutter - buckets). The baker's engine module (`src/bake_root.zig`) adds `portray.zig` + - the C/Lua sources on top of the pure `root.zig`; the unit-test build stays pure - Zig. On a glibc Linux host the baker links against Zig's own static musl (the - self-hosted ELF linker rejects a modern glibc `crt1.o`'s `.sframe` - relocations). Portrayal failure (e.g. rules dir not found) falls back to - `classify()` as before. Default rules dir resolves like the C++ host - (`--rules`, else `TILE57_S101_RULES`, else the vendored catalogue). -- **Honest portrayal diagnostics.** A feature whose S-101 class has no rule file - (e.g. SweptArea/SWPARE — an IHO catalogue gap) is now counted as *unportrayed* - rather than an *error*, matching the Go reference's silent suppression; the - per-error log prints the primitive's name (`Point`/`Curve`/`Surface`) instead - of a useless `table: 0x…` address. (Shared `csrc/lua_shim.c` driver, so the - live path benefits too.) -- **Native S-52 fallback for SweptArea (SWPARE).** The S-101 catalogue ships no - SweptArea rule (an IHO gap), so the Lua engine emits nothing for it. The MVT - layer now draws the Go reference's `sweptAreaBuild` fallback directly: a dashed - `CHGRD` boundary on each ring, the `SWPARE51` swept-depth bracket at the area's - representative point, and a `swept to ` label. (`engine/src/s57_mvt.zig`, - shared by the baker and the live path.) -- **Native S-52 fallback for NEWOBJ.** NEWOBJ-derived features (e.g. - VirtualAISAidToNavigation) whose S-101 rule doesn't portray the encoded geometry - — wrong primitive, unofficial stub — now draw the Go reference's `newObjectBuild` - placeholder, a dashed `CHMGF` (magenta) outline on the line/area geometry, rather - than being dropped. A genuine rule error on any *other* class is still suppressed - (no output), matching the Go reference. (`engine/src/s57_mvt.zig`.) -- **SCAMIN decluttering**: the live path now routes features carrying SCAMIN - (attr 133) into `*_scamin` MVT buckets, and the generated style gives those layers - a per-feature `minzoom` derived from the SCAMIN 1:N denominator, so minor - features drop out below their scale (replacing the M1 "both shown" stub). -- **S-52 draw priority**: `draw_prio` (from the S-101 instruction stream) is now - emitted on features and used as the area fill-sort-key (Go sync `3ca4d5f`). -- **Drying-line contour**: 0 m `VALDCO` is emitted on DEPCNT lines + a - line-centre contour-label style layer (Go sync `f86b750`). -- **M_QUAL data quality**: `zoneOfConfidence` is synthesized from CATZOC (via a - generalized complex-attribute binding) so `QualityOfBathymetricData` portrays - (Go sync `49e9cd9`). -- QUAPOS quality-of-position is parsed + aggregated per feature (Go sync - `1b04ebb`); the approximate-position dashed-line *application* is still to come. -- Derived depth attrs (`defaultClearanceDepth`/`surroundingDepth`, Go sync - `a9c8afd`) are computed + supplied to under/awash dangers; combined with the - spatial binding above this clears the danger-rule crashes. Blank S-57 attributes - are now treated as absent (they were served as `""`, building a malformed - `ScaledDecimal{Value=nil}` that crashed the depth comparison). - -### Fixed -- **Guaranteed attribute bindings no longer clobber the catalogue's multiplicity.** - `inTheWater`/`orientationValue`/`topmark` were bound unconditionally (Upper=1) - *after* the catalogue bindings, overwriting a feature type's real multiplicity — - e.g. RadioCallingInPoint's array-valued `orientationValue` (Upper=2), so the rule - crashed indexing `feature.orientationValue[1]`. Now added only when the catalogue - doesn't already bind them (matches Go's `withGuaranteed`). Clears the - RadioCallingInPoint errors on real NOAA cells; the directional RDOCAL symbols and - labels now portray. (`engine/csrc/lua_shim.c`.) - -### Changed — two-library architecture + C APIs (breaking, pre-1.0) -- The project is now two C libraries with distinct roles: - - **`libtile57`** — the Zig S-57 tile generator (was `libtilegen.a` / `tg_*`). - Prefix `tile57_*`, headers `include/tile57.h` + `include/tile57_diag.h`. The - sources live under `engine/` (Zig module `engine`). Env var - `TILE57_S101_RULES`. - - **`libchartplotter`** — the embeddable chart **widget** that opens a window - and renders (it wraps MapLibre Native + libtile57). Prefix `chartplotter_*`, - header `include/chartplotter.h`. (`libchartplotter` previously named the tile - generator; that role is now `libtile57`.) -- Redesigned the tile-source C ABI: one opener - `tile57_source_open(data, len, format, rules_dir)` with a `TILE57_FORMAT_AUTO` - sniff (replacing two openers + the duplicated try/fallback) plus - `tile57_source_open_cells` for an ENC_ROOT; `tile57_source_format/zoom_range/ - bounds/clear_cache`, a named `tile57_tile_status`, `tile57_version`, and the - S-101 rules dir as an argument (env `TILE57_S101_RULES` is only a NULL - fallback). Lifetime + threading contracts documented in the header. -- App binaries: `chart-glfw-zig` → **`chartplotter`** (window), `chartshot-zig` - → **`chartplotter-render`** (headless PNG); both are now thin mains over - `libchartplotter`. C++ FileSource adapter `ZigTileSource` → **`ChartTileSource`**. - -### Removed -- The dead `CHART_ASYNC` worker path in the tile-source adapter (an artifact of - the flicker investigation; the flicker is fixed at its source, so the worker - was unused and made the threading contract ambiguous). -- Dead `#import ` in `app/metal_backend.mm` (left over - from the reverted `CATransaction`/`presentsWithTransaction` attempts). - -### Fixed -- **macOS interactive flicker** (the headline fix): `ChartTileSource` now issues - conditional tile requests (stable `etag` + `notModified`), so MapLibre reuses - its already-parsed tiles instead of re-parsing + re-uploading them 15-60×/sec. -- Low idle CPU in the interactive window via on-demand rendering (default; - `CHART_CONTINUOUS=1` opts back into present-every-frame). -- macOS Metal `nextDrawable` nil-guard + `setAllowsNextDrawableTimeout(false)` - to stop whole-screen blanks on fast pan; async present matching MapLibre's - macOS SDK. -- macOS link warning: the Zig archive is now built for the same deployment - target as the C++ host (`CMAKE_OSX_DEPLOYMENT_TARGET`, default 14.3). - -### Added -- **`libchartplotter` chart-widget library**: `chartplotter_view_open/run/close` - (open an interactive window) + `chartplotter_render_png` (headless) + the - `chartplotter_view_options` (size/title/style/rules/camera). The MapLibre glue - the two hosts used to duplicate now lives once in the library. -- **ENC_ROOT support**: point a host at a directory to load every base cell plus - its sequential `.001…` update files, overlaid. New - `tile57_source_open_cells` C API + a `cells` backend - (`s57_mvt.generateTileMulti`); the host walks the directory (`app/enc_root.hpp`). -- **S-57 update application** (`parseCellWithUpdates`): record-level merge by FOID - / (RCNM,RCID) with RUIN insert/delete/modify and the SGCC/FSPC control fields. -- M3 interactive pan/zoom window (`chartplotter`) serving live Zig tiles. -- Live S-57 → MVT portrayal now emits soundings (SNDFRM04 glyphs), area fill - patterns, and feature names (synthesized from OBJNAM). -- Area labels placed at the true centroid when inside (Go sync `30db686`). -- In-process tile cache in the generator (memoizes generated/decoded tiles). -- A **Docusaurus documentation site** under `docs/` (Introduction, Installation, - Getting Started, C API, Architecture, Tile Schema, Known Limitations), - deployed to GitHub Pages; this changelog; `specs/go-sync.md` Go-port plan. -- MIT `LICENSE`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9064e36 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# tile57 — engine conventions + +## The API style + +The public surface (include/tile57.h, src/tile57.zig, bindings/go) follows one +style. Hold every new or renamed symbol against it. + +**The pipeline is the organizing principle.** Everything is bake, then compose +(or bake, then render): source charts bake once to per-chart archives; every +output is produced from baked archives. Sections, in order: Version, Errors, +Bake, Render (the chart), Compose, Style, Util. The header, the Zig public +root, and the docs all follow that same section order. + +**The unit is a chart, never "cell" in prose.** An ENC cell IS one chart at one +compilation scale. "Cell" survives only as spec-facing vocabulary: one +first-use definition ("a *cell*, in the spec's vocabulary"), the `cell` tile +property and other wire-format names, `` CLI placeholders and the +`cell`/`cells` CLI commands, and the s57/geometry module internals (the domain +that parses cells). + +**Everything is in a family, and the family token always leads.** The prefix +names what a call runs against; never bury the family token mid-name. + +1. *Handles namespace under their noun and mirror each other.* `tile57_chart` + (one open baked archive) and `tile57_compose` (the runtime compositor) are + peers: `tile57_chart_open` / `_close` / `_get_info` / `_tile` / `_png` / + `_query` ↔ `tile57_compose_open` / `_close` / `_get_meta` / `_tile` / + `_png` / `_query`. A noun prefix means the first parameter is that handle — + the C spelling of a method. +2. *Handle-free families lead with their section token, then what the call + acts on or yields* — the token's part of speech follows the section, so the + name reads naturally after it. `tile57_bake_*` is a verb family + (`bake_chart_bytes`, `bake_tree` — "bake the X"); `tile57_enc_*` is a domain + family (the raw S-57 source readers — ENC is the ONE vocabulary for raw + source data, never s57_/source_/scan_ variants); `tile57_style_*` is a + product family (`style_build` / `style_diff` / `style_template`, never + `build_style` — "the style's build/diff/template"). Small POD/domain noun + families follow the same shape: `pmtiles_metadata`, `status_str`, + `mariner_defaults`, `colortables_default`, `assets_free`. +3. *Bare `tile57_` is the library itself* — process/library-level plumbing + only: `tile57_version`, `tile57_free`, `tile57_warmup`. No handle method is + ever bare. + +In Zig the namespace is the family, with the same two consequences: a member +never repeats its namespace (`ComposeSource.openFiles`, never +`compose.openComposeSourceFiles`; `compose.tile`, never `compose.composeTile`), +and the two handles mirror (`Chart.openPmtilesPath` ↔ `ComposeSource.open` / +`.openFiles`). + +**Outputs are named by what you get, never by how it's produced or served.** +`tile`, `png`, `pdf`, `canvas`, `surface`, `query` — not serve/render/build/ +generate. The chart and the compositor offer the SAME output set, so the names +mirror exactly: `tile57_chart_png` ↔ `tile57_compose_png`, `tile57_chart_tile` +↔ `tile57_compose_tile`, and so on. The compositor is "many charts in, one +chart out"; keep that symmetry when adding an output — it goes on both handles +or has a stated reason not to. + +**The status model is universal.** Every call that can fail returns +`tile57_status` (never a bare int, count, or bool) and takes an optional +caller-owned `tile57_error*` as its LAST parameter. Out-parameters come after +inputs and are ALWAYS defined on return — the result on TILE57_OK, NULL/0 +otherwise. "Nothing produced" is NOT a failure: OK with a NULL/zero out. Every +pointer argument is BADARG-checked (no NULL derefs across the ABI). Counts and +flags come back through optional out-pointers (NULL to ignore), not return +values. Say "call that can fail" in docs, not "fallible". + +**Memory has one rule.** Calls that return bytes allocate `*out`; the caller +releases with `tile57_free(ptr)` — buffers are length-prefixed at allocation, +so the pointer is all it needs. POD across the seam: no Zig errors, slices, or +optionals cross the ABI. + +**Ownership and borrowing are explicit.** The compositor BORROWS its charts +(charts outlive it; close the compositor first). A path-opened chart mmaps its +file (the file stays in place). No handle is internally synchronized; document +lifetime + threading on every handle-producing call. + +**The Zig public root is curated, not a module mirror.** src/tile57.zig shapes +the public namespace by the API sections; when module layering puts an +implementation elsewhere (e.g. composed view renders live beside Chart because +the `compose` module is a dependency leaf without the render path), the root +still surfaces it under the name it belongs to (`tile57.compose.renderView`). +Public surface trumps internal layering. + +**Go bindings track shape, not spelling.** Idiomatic Go names over the same +structure: `Open`/`OpenBytes` on the archive, package-level `Charts`/`Features`/ +`FeaturesBytes`/`CatalogEntries` for the enc readers, `ComposeSource.Tile` +mirroring `tile57_compose_tile`, errors as wrapped sentinels +(`errors.Is(err, tile57.ErrParse)`). + +## Other repo rules + +- Never reference specs/*.md in code comments (specs/ is never committed); + describe designs inline. +- Docs read standalone: present-tense capability statements, no prior-version + framing ("now supports", "anymore"), no "plain language", no "fallible", no + "seam" in user-facing docs. +- Never `git add -A` (vendor/ submodules; charts/ + test/ hold huge untracked + local data). Stage explicit paths. Commit after every change. +- Never hardcode machine-specific paths in code or tests; real-cell paths come + from args/env. +- Validate conversion/portrayal changes on real ENC_ROOT cells, not just unit + tests. diff --git a/README.md b/README.md index 5632372..742cce5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

tile57

- ⚓ A high-performance, low-memory S-57/S-101 chart engine: vector tiles, S-52 styles, PNG and PDF.
- tile57 turns IHO S-57 ENC cells into Mapbox Vector Tiles with a matching MapLibre S-52 - style, or renders finished charts directly to PNG and PDF — one Zig library with a + ⚓ An S-57/S-101 chart engine: vector tiles, S-52 styles, PNG and PDF.
+ tile57 turns IHO S-57 ENC cells into vector tiles (MLT or MVT) with a matching MapLibre + S-52 style, or renders finished charts directly to PNG and PDF — one Zig library with a C ABI, compiled natively or to WASM.

@@ -48,11 +48,10 @@ entirely with AI assistance. A few specific goals shape its design: - **Language-agnostic embedding.** A thin C ABI (`libtile57.a`) bridges the Zig core to any language with C FFI. Go bindings ship in the repo; others are straightforward additions. -- **An engine anyone can build on.** The end goal is an easy-to-use S-57/S-100 chart - rendering engine that anyone can use to build their marine app ideas — without first - becoming an IHO spec expert. Open a chart, get tiles, PNGs, or PDFs; the S-52 rules, - portrayal catalogue, and mariner settings are the engine's problem. Ideas it should make - easy: +- **An engine to build on.** The goal is an S-57/S-100 chart engine you can use to build + a marine app without first becoming an IHO spec expert. Open a chart, get tiles, PNGs, + or PDFs; the S-52 rules, portrayal catalogue, and mariner settings are the engine's + problem. It aims to support: - an **anchor alarm** that draws your swing circle over a real chart, - a **Windy plugin** overlaying forecast weather on ENC charts, - a **native cross-platform Qt6 C++ chartplotter**, @@ -61,24 +60,24 @@ entirely with AI assistance. A few specific goals shape its design: --- **tile57** decodes NOAA/IHO **S-57** ENC cells and generates **vector tiles** by -`(z, x, y)` — MapLibre Tiles (MLT, the default bake format; MapLibre GL JS ≥ 5.12 -decodes them natively) or Mapbox Vector Tiles (`--format mvt`) — running the +`(z, x, y)` — MapLibre Tiles (MLT, the default; MapLibre GL JS ≥ 5.12 +decodes them natively) or Mapbox Vector Tiles — running the official IHO **S-101 Portrayal Catalogue** in embedded Lua to produce S-52 nautical portrayal. Alongside the tiles it emits a **MapLibre GL style** and the portrayal **assets** it references — colour tables, line styles, and the sprite + area-fill pattern atlases — so a renderer like [MapLibre](https://github.com/maplibre/maplibre-native) can draw a chart directly. -It is **high-performance and low-memory** by design: +It holds only its working set: - **Lazy, per-cell work.** A multi-cell ENC_ROOT is indexed cheaply (band + bbox); cells are parsed and portrayed only when a requested tile needs them, then held under an LRU bound. A **streaming** open reads a cell's bytes on demand (and frees them on eviction), so a host holds only the working set — not the whole catalogue. -- **Band-streamed bakes.** Baking an ENC_ROOT to one PMTiles archive streams - band-by-band (finest → coarsest, best-band dedup), so peak memory tracks the - largest single band. +- **Per-cell bakes.** Each ENC cell bakes to its own PMTiles at its compilation + scale, so a bake holds one cell at a time; the runtime compositor stitches the + cells by `(z, x, y)` on demand. - **Pure-Zig core.** The foundational format/encode packages have no libc; only the Lua portrayal + sprite rasterizer pull in C. @@ -86,21 +85,24 @@ It is **high-performance and low-memory** by design: ``` S-57 ENC cell (.000) - │ ISO 8211 decode src/s57/iso8211.zig + │ ISO 8211 decode src/iso8211/ ▼ S-57 feature + geometry model src/s57/ - │ S-101 portrayal (embedded Lua) src/portray/ + src/s100/ + │ adapt S-57 → S-101 features src/s101/ (adapter) ▼ -portrayal instruction stream +S-101 feature records + │ S-101 portrayal (embedded Lua) src/portray/ + rules + ▼ +portrayal instruction stream src/s101/ (instructions) │ scene generation src/scene/ (project + clip + draw calls) ▼ render Surface ──► MVT / MLT tiles (src/tiles/) + MapLibre style.json + assets └───► PNG raster / vector PDF / terminal text (src/render/) ``` -The stages are separate Zig modules — `s57` (including its ISO 8211 decoder), -`s100`, `tiles`, `render`, `scene`, `assets` — pure Zig with no libc; only the -Lua portrayal (`portray`) and the sprite rasterizer (`sprite`) pull in C. See +The stages are separate Zig modules — `iso8211`, `s57`, `s101`, `tiles`, +`render`, `scene`, `style` — pure Zig with no libc; only the Lua portrayal +(`portray`) and the sprite rasterizer (`sprite`) pull in C. See [the architecture docs](docs/docs/architecture.md). ## Use it from Zig @@ -114,32 +116,36 @@ const tile57 = @import("tile57"); var chart = try tile57.Chart.openPath("ENC_ROOT/", null, true); defer chart.deinit(); -if (try chart.tile(z, x, y)) |mvt| { // decompressed MVT bytes, or null if empty - defer tile57.freeBytes(mvt); - // … hand to your renderer … -} +const bbox = chart.bounds(); // geographic extent [w, s, e, n], or null +// … render a view (chart.renderView), query features, or bake an archive … ``` -`Chart.openPath` streams cells on demand (working-set only); `Chart.openBytes` opens a -single in-memory cell. See [the Zig API docs](docs/docs/zig-api.md). +`Chart` renders views, queries features, and reads metadata. The runtime tile +path — bake each cell, then compose by `(z, x, y)` on demand — is exposed through +the [C ABI](include/tile57.h). See [the Zig API docs](docs/docs/zig-api.md). ## Use it from C -The same engine behind a thin C ABI ([`include/tile57.h`](include/tile57.h)): +The same engine behind a thin C ABI ([`include/tile57.h`](include/tile57.h)). +Tiles are made one way — bake each cell to its own PMTiles, then compose on +demand — the structure `tile57 bake ENC_ROOT -o out/` writes: ```c -// Open an on-disk ENC_ROOT directory (or a single .000 file). -tile57_chart *chart = tile57_chart_open("ENC_ROOT/"); -uint8_t *mvt; size_t n; -if (tile57_chart_tile(chart, z, x, y, &mvt, &n) == TILE57_TILE_OK) { - /* … render mvt … */ - tile57_free(mvt, n); +// out/ holds tiles/.pmtiles (one per cell) + partition.tpart +const char *paths[] = { "out/tiles/US5MD1MC.pmtiles" }; +tile57_compose_source *src = tile57_compose_open(paths, 1, "out/partition.tpart"); + +uint8_t *tile; size_t n; +if (tile57_compose_serve(src, z, x, y, &tile, &n) == 1) { /* decompressed MLT */ + /* … hand the tile to your renderer … */ + tile57_free(tile, n); } -tile57_chart_close(chart); +tile57_compose_close(src); ``` -`libtile57.a` also exposes the ENC_ROOT bake, the MapLibre style builder, and the -asset/atlas generators. See [the C API docs](docs/docs/c-api.md). +A separate `tile57_chart` handle renders finished PNG/PDF views and answers +metadata + object queries; `libtile57.a` also exposes the MapLibre style builder +and the asset/atlas generators. See [the C API docs](docs/docs/c-api.md). ## The `tile57` CLI @@ -147,8 +153,8 @@ The offline tool bakes charts and emits portrayal assets: ```sh zig build # builds zig-out/bin/tile57 -tile57 bake CELL.000 -o out/ # one cell -> bundle (tiles + style + assets + manifest) -tile57 bake ENC_ROOT -o out/ # whole catalogue, band-streamed -> same bundle +tile57 bake CELL.000 -o out/ # one cell -> out/tiles/.pmtiles + partition.tpart +tile57 bake ENC_ROOT -o out/ # whole catalogue -> per-cell tiles/ + partition.tpart tile57 assets -o assets/ # colortables + linestyles + sprite + patterns tile57 png ENC_ROOT --view -76.48,38.974,15 --size 1600x1200 -o chart.png tile57 pdf ENC_ROOT --view -76.48,38.974,15 --size 1600x1200 -o chart.pdf @@ -172,7 +178,7 @@ Docs source lives in [`docs/`](docs/): [intro](docs/docs/intro.md), [getting started](docs/docs/getting-started.md), the [Zig API](docs/docs/zig-api.md), the [C API](docs/docs/c-api.md), the [architecture](docs/docs/architecture.md), and the -[tile schema](docs/docs/tile-schema.md). See also [`CHANGELOG.md`](CHANGELOG.md). +[tile schema](docs/docs/tile-schema.md). ## License diff --git a/bindings/go/README.md b/bindings/go/README.md index 8bdbd59..e37a788 100644 --- a/bindings/go/README.md +++ b/bindings/go/README.md @@ -34,25 +34,39 @@ replace github.com/beetlebugorg/tile57/bindings/go => /path/to/chartplotter-nati ```go import tile57 "github.com/beetlebugorg/tile57/bindings/go" -// Bake a single cell.000 or a whole ENC_ROOT dir into a self-contained bundle. -cells, bbox, err := tile57.BakeBundle("/enc/ENC_ROOT", "/out/bundle", "", "", "", 0, 16, nil) +// Bake an ENC_ROOT: each cell becomes its own PMTiles under /tiles/, plus +// an ownership partition at /partition.tpart. +n, err := tile57.BakeTree("/enc/ENC_ROOT", "/out", 4, nil) -// Or serve tiles live, and publish the SCAMIN manifest for the style. -src, _ := tile57.Open("/enc/ENC_ROOT") +// Open the compositor over the baked archives + partition, and serve tiles. +src, _ := tile57.OpenCompose([]string{"/out/tiles/US5MD1MC.pmtiles"}, "/out/partition.tpart") defer src.Close() -mvt, _ := src.Tile(13, 2359, 3139) -manifest := src.Scamin() // []uint32, ascending +body, owned, _ := src.Tile(13, 2359, 3139) // owned=false, body=nil => open ocean + +// Or open one baked archive as a chart (bounds, scale, coverage, SCAMIN). +chart, _ := tile57.Open("/out/tiles/US5MD1MC.pmtiles") +defer chart.Close() +info := chart.Info() // zoom range, bounds, embedded 1:N scale +scamin := chart.Scamin() // []uint32, ascending + +// Raw S-57 reading is handle-free (cell inventory, feature extraction). +charts, _ := tile57.Charts("/enc/ENC_ROOT") +water, _ := tile57.Features("/enc/ENC_ROOT/US5MD1MC/US5MD1MC.000", "DEPARE", "DRGARE") ``` ## Surface -- **Charts** — `Open` (path, streaming), `OpenChartBytes` (one in-memory cell), - `OpenPMTiles` (baked bundle); `Source.Tile`, `Info`, `Meta`, `Scamin`, - `ClearCache`, `Close`. -- **Bake** — `BakeCells` (→ one PMTiles archive in memory), `BakeBundle` (→ a full - on-disk bundle: tiles + assets + per-scheme styles + manifest). -- **Assets / style** — `ColortablesDefault`, `Colortables`, `Linestyles`, - `SpriteAtlas`, `PatternAtlas`, `StyleTemplate`, `BuildStyle`, `Style`, +- **Charts (a baked archive: metadata + query)** — `Open` (path, mmap'd), + `OpenBytes`; `Source.Info`, `Meta`, `Scamin`, `Coverage`, `Close`. +- **S-57 source readers (handle-free)** — `Charts` (per-chart metadata of a .000 or + ENC_ROOT), `Features` / `FeaturesBytes` (GeoJSON extraction), `CatalogEntries` + (CATALOG.031 decode). +- **Bake** — `BakeChart` (one chart → PMTiles bytes), `BakeTree` (an ENC_ROOT → per-chart + archives), `BakeAssets` (portrayal assets in memory). +- **Compose** — `OpenCompose` (paths; owns its charts) / `OpenComposeCharts` + (borrows yours); `ComposeSource.Serve` (a tile, with an ownership flag), `Meta`, + `SavePartition`, `Close`. +- **Style** — `ColortablesDefault`, `Style`, `BuildStyle`, `StyleDiff`, `MarinerDefaults`. `libtile57` is not internally synchronized; every `Source` method is mutex-guarded, diff --git a/bindings/go/asset.go b/bindings/go/asset.go index d3f2e08..90e4894 100644 --- a/bindings/go/asset.go +++ b/bindings/go/asset.go @@ -9,7 +9,6 @@ package tile57 import "C" import ( - "fmt" "unsafe" ) @@ -19,8 +18,9 @@ import ( func ColortablesDefault() ([]byte, error) { var out *C.uint8_t var n C.size_t - if C.tile57_colortables_default(&out, &n) != 1 { - return nil, fmt.Errorf("tile57: colortables_default failed") + var cerr C.tile57_error + if st := C.tile57_colortables_default(&out, &n, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return tileBytes(out, n), nil } @@ -39,8 +39,9 @@ func BakeAssets(catalogDir string) (Assets, error) { cdir, free := cStringOrNil(catalogDir) defer free() var ca C.tile57_assets - if C.tile57_bake_assets(cdir, &ca) != 1 { - return Assets{}, fmt.Errorf("tile57: bake_assets failed") + var cerr C.tile57_error + if st := C.tile57_bake_assets(cdir, &ca, &cerr); st != C.TILE57_OK { + return Assets{}, statusError(st, &cerr) } defer C.tile57_assets_free(&ca) return Assets{ diff --git a/bindings/go/bake.go b/bindings/go/bake.go index 15a101e..a0922eb 100644 --- a/bindings/go/bake.go +++ b/bindings/go/bake.go @@ -6,51 +6,8 @@ package tile57 #include #include "tile57.h" -// Forward declaration of the //export'd Go progress callback (defined in -// progress.go). The trampoline lets us hand libtile57 a real C function pointer -// that re-enters Go, without taking the address of a Go func in Go code. -void tile57GoBakeProgress(void *user, uint8_t stage, size_t done, size_t total, - uint8_t band_index, uint8_t band_count, const char *band_name); - -// user != NULL → re-enter Go for progress; NULL → no progress (bake_pmtiles has no -// console default, and its trampoline no-ops on a NULL user regardless). -static int tile57_bake_pmtiles_cb(const tile57_cell *cells, size_t count, - const char *rules_dir, uint8_t minz, uint8_t maxz, - int omit_pick_attrs, uint8_t format, - void *user, uint8_t **out, size_t *out_len) { - tile57_bake_opts opts = { - .rules_dir = rules_dir, - .catalog_dir = NULL, - .created = NULL, - .minzoom = minz, - .maxzoom = maxz, - .omit_pick_attrs = omit_pick_attrs != 0, - .progress = user ? tile57GoBakeProgress : (tile57_bake_progress)0, - .progress_user = user, - .format = format, - }; - return tile57_bake_pmtiles(cells, count, &opts, out, out_len); -} - -// user != NULL → re-enter Go for progress; NULL → the lib's built-in console. -static int tile57_bake_bundle_cb(const char *input, const char *out_dir, - const char *rules_dir, const char *catalog_dir, - const char *created, uint8_t minz, uint8_t maxz, - int omit_pick_attrs, uint8_t format, - void *user, uint32_t *out_cells, double *out_bbox) { - tile57_bake_opts opts = { - .rules_dir = rules_dir, - .catalog_dir = catalog_dir, - .created = created, - .minzoom = minz, - .maxzoom = maxz, - .omit_pick_attrs = omit_pick_attrs != 0, - .progress = user ? tile57GoBakeProgress : (tile57_bake_progress)0, - .progress_user = user, - .format = format, - }; - return tile57_bake_bundle(input, out_dir, &opts, out_cells, out_bbox); -} +// Trampoline: the C progress callback calls back into this exported Go function. +extern void tile57GoBakeProgress(void *ctx, uint32_t done, uint32_t total); */ import "C" @@ -60,109 +17,139 @@ import ( "unsafe" ) -// BakeOpts are the shared knobs for [BakePmtiles] and [BakeBundle]. The zero value -// is the default: the catalogue embedded in libtile57, no band clamp, and the -// per-feature pick-report attributes included. CatalogDir and Created apply to -// [BakeBundle] only ([BakePmtiles] ignores them). -type BakeOpts struct { - RulesDir string // "" = embedded portrayal rules - CatalogDir string // "" = embedded S-101 catalogue (BakeBundle only) - Created string // "" = manifest "created" unset (BakeBundle only) - MinZoom uint8 // 0/0 = no band clamp - MaxZoom uint8 - OmitPickAttrs bool // true = drop the pick-report attrs (class/cell/s57) for a leaner bake - Format TileFormat // baked tile encoding; the zero value bakes the engine default (MLT) +//export tile57GoBakeProgress +func tile57GoBakeProgress(ctx unsafe.Pointer, done, total C.uint32_t) { + if fn, ok := cgo.Handle(uintptr(ctx)).Value().(func(int, int)); ok && fn != nil { + fn(int(done), int(total)) + } } -// BakePmtiles bakes an ENC_ROOT's in-memory cells into ONE zoom-banded PMTiles -// archive (write it to a file and open cheaply via [OpenPMTiles], instead of -// generating tiles live). opts.MinZoom/MaxZoom clamp the per-cell bands (0/0 = no -// clamp); opts.CatalogDir/Created are ignored here. progress, if non-nil, is called -// with a [BakeProgress] per update: stage 0 = loading/portraying cells, stage 1 = -// baking tiles (per-band done/total + band label). -func BakePmtiles(cells []Cell, opts BakeOpts, progress func(BakeProgress)) ([]byte, error) { - if len(cells) == 0 { - return nil, fmt.Errorf("tile57: no cells to bake: %w", ErrEmptyInput) +// BakeChart bakes ONE on-disk chart (a .000 path + its .001.. updates) to a PMTiles +// archive over its NATIVE band zoom range and returns the bytes — the per-chart tile +// store the compositor consumes (it handles any cross-band zoom expansion). The +// archive metadata embeds that chart's own coverage (M_COVR + cscl + date/name); +// read it back with [PMTilesMetadata], or open the archive with [OpenBytes]. A +// chart that produces no tiles returns ErrNoCoverage. +func BakeChart(path string) ([]byte, error) { + if path == "" { + return nil, fmt.Errorf("tile57: BakeChart needs a chart path: %w", ErrEmptyInput) } - cRules, freeRules := cStringOrNil(opts.RulesDir) - defer freeRules() + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) - arena := &cArena{} - defer arena.free() - _, base := arena.cellInputs(cells) + var out *C.uint8_t + var outLen C.size_t + var cerr C.tile57_error + if st := C.tile57_bake_chart_bytes(cPath, &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) + } + if out == nil { + return nil, fmt.Errorf("tile57: chart bake produced no tiles: %w", ErrNoCoverage) + } + return tileBytes(out, outLen), nil +} - var user unsafe.Pointer - if progress != nil { - h := cgo.NewHandle(progress) +// BakeTree walks inDir for S-57 base charts (*.000) and bakes each IN PARALLEL to the SAME relative +// path under outDir with a .pmtiles extension (+ an .sha content-hash sidecar), creating +// subdirs. INCREMENTAL: a chart whose archive is already up to date (newer than its .000 and its +// update chain) is skipped, so a re-run over an unchanged tree bakes nothing — 0 over a warm cache +// is success. The engine writes and frees each archive as it goes, so this never holds N archives +// in memory (peak ~ workers). outDir is the caller's OWN cache — it owns the location + layout, so +// distinct consumers don't clash. onProgress(done, total) fires per baked chart (may be called +// concurrently from workers, so it must be safe for concurrent use). Returns the number baked +// THIS run. +func BakeTree(inDir, outDir string, workers int, onProgress func(done, total int)) (int, error) { + if inDir == "" || outDir == "" { + return 0, fmt.Errorf("tile57: BakeTree needs input + output dirs: %w", ErrEmptyInput) + } + if workers < 1 { + workers = 1 + } + cIn := C.CString(inDir) + defer C.free(unsafe.Pointer(cIn)) + cOut := C.CString(outDir) + defer C.free(unsafe.Pointer(cOut)) + + var cb C.tile57_bake_progress + var ctx unsafe.Pointer + if onProgress != nil { + h := cgo.NewHandle(onProgress) defer h.Delete() - user = unsafe.Pointer(&h) + cb = C.tile57_bake_progress(C.tile57GoBakeProgress) + ctx = unsafe.Pointer(h) //nolint:govet // cgo.Handle passed as the void* ctx, retrieved verbatim + } + var baked C.uint32_t + var cerr C.tile57_error + if st := C.tile57_bake_tree(cIn, cOut, C.uint32_t(workers), cb, ctx, &baked, &cerr); st != C.TILE57_OK { + return 0, statusError(st, &cerr) } + return int(baked), nil +} +// PMTilesMetadata returns a PMTiles archive's metadata JSON blob (decompressed), or +// nil if the archive carries none. A [BakeChart] archive embeds the chart's coverage +// under a "coverage" key. The pmtiles bytes are read but not retained. +func PMTilesMetadata(pmtiles []byte) ([]byte, error) { + if len(pmtiles) == 0 { + return nil, fmt.Errorf("tile57: PMTilesMetadata needs archive bytes: %w", ErrEmptyInput) + } var out *C.uint8_t var outLen C.size_t - rc := C.tile57_bake_pmtiles_cb(base, C.size_t(len(cells)), cRules, - C.uint8_t(opts.MinZoom), C.uint8_t(opts.MaxZoom), cOmit(opts.OmitPickAttrs), C.uint8_t(opts.Format), user, &out, &outLen) - switch rc { - case 1: - return tileBytes(out, outLen), nil - case 0: - return nil, fmt.Errorf("tile57: bake produced no tiles: %w", ErrNoCoverage) - default: - return nil, fmt.Errorf("tile57: bake failed") + var cerr C.tile57_error + if st := C.tile57_pmtiles_metadata((*C.uint8_t)(unsafe.Pointer(&pmtiles[0])), C.size_t(len(pmtiles)), &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } + if out == nil { + return nil, nil // archive has no metadata + } + return tileBytes(out, outLen), nil } -// BakeBundle bakes an on-disk input (a .000 cell or an ENC_ROOT directory) into a -// self-contained chart bundle under outDir — the canonical, drift-proof package: -// -// outDir/tiles/chart.pmtiles (PMTiles + scamin & vector_layers metadata) -// outDir/assets/colortables.json, linestyles.json, sprite-mln{,@2x}.{json,png} -// outDir/assets/style-{day,dusk,night}.json (per-scheme, SCAMIN-bucketed) -// outDir/manifest.json (schema_version, bbox, cells, styles) -// -// opts.RulesDir/CatalogDir "" use the catalogue embedded in libtile57. opts.Created -// "" leaves the manifest timestamp unset. opts.MinZoom/MaxZoom clamp the per-cell -// bands (0/0 → no clamp). progress nil uses the lib's built-in console progress. -// Returns the cell count and bbox (west,south,east,north). +// PartitionBand selects which ownership-partition map [BakePartitionDebug] emits. +type PartitionBand int8 + +const ( + // BandGoverning emits, at each zoom, the partition of the band that governs it — + // the natural view (coarse bands zoomed out, finer bands zoomed in). + BandGoverning PartitionBand = -1 + // The six navigational-purpose bands, finest→coarsest: each emits ITS OWN + // partition map at every zoom (e.g. BandHarbor is the finest quilt). + BandBerthing PartitionBand = 0 + BandHarbor PartitionBand = 1 + BandApproach PartitionBand = 2 + BandCoastal PartitionBand = 3 + BandGeneral PartitionBand = 4 + BandOverview PartitionBand = 5 +) + +// BakePartitionDebug bakes the ownership-partition DEBUG tiles from an on-disk +// ENC_ROOT into a single PMTiles at outPath — the composited faces (which chart renders +// which ground at each band), one polygon per owning chart in a layer named "partition" +// with the properties cell/cscl/band/tier/oi/color, and NO portrayed chart content. It +// is the raw material for a partition-debug UI: point a MapLibre style (using the +// vector_layers metadata) at it and fill by the "color" property. // -// progress reports a [BakeProgress] per update: stage 0 = loading cells -// (Done/Total = cells); stage 1 = baking tiles, where Done/Total are PER BAND -// (reset each band) and Total is that band's planned tile count — so a UI can show -// a per-band percentage. BandIndex/BandCount/BandName give the band label -// ("approach", band 3/6). The planned total slightly over-counts the emitted tiles -// (empty tiles are skipped), matching the Go baker's planned bar. -func BakeBundle(input, outDir string, opts BakeOpts, progress func(BakeProgress)) (cellCount int, bbox [4]float64, err error) { - if input == "" || outDir == "" { - return 0, bbox, fmt.Errorf("tile57: BakeBundle needs input and out dir: %w", ErrEmptyInput) +// band = [BandGoverning] emits the band governing each zoom (the natural view); +// [BandBerthing]…[BandOverview] emit that one band's own map at every zoom. minZoom and +// maxZoom bound the tiles — the coarse bands are cheap, but harbor-level detail (maxZoom +// >= 13) multiplies the tile count ~4× per zoom, so raise it deliberately. Returns the +// chart count; nothing covered returns ErrNoCoverage (no file written). +func BakePartitionDebug(encRoot, outPath string, minZoom, maxZoom uint8, band PartitionBand) (chartCount int, err error) { + if encRoot == "" || outPath == "" { + return 0, fmt.Errorf("tile57: BakePartitionDebug needs an ENC root and out path: %w", ErrEmptyInput) } - cIn := C.CString(input) - defer C.free(unsafe.Pointer(cIn)) - cOut := C.CString(outDir) + cRoot := C.CString(encRoot) + defer C.free(unsafe.Pointer(cRoot)) + cOut := C.CString(outPath) defer C.free(unsafe.Pointer(cOut)) - cRules, fr := cStringOrNil(opts.RulesDir) - defer fr() - cCat, fc := cStringOrNil(opts.CatalogDir) - defer fc() - cCreated, fcr := cStringOrNil(opts.Created) - defer fcr() - var user unsafe.Pointer - if progress != nil { - h := cgo.NewHandle(progress) - defer h.Delete() - user = unsafe.Pointer(&h) + var charts C.uint32_t + var cerr C.tile57_error + if st := C.tile57_bake_partition_debug(cRoot, cOut, C.uint8_t(minZoom), C.uint8_t(maxZoom), C.int8_t(band), &charts, &cerr); st != C.TILE57_OK { + return 0, statusError(st, &cerr) } - - var cells C.uint32_t - var bb [4]C.double - rc := C.tile57_bake_bundle_cb(cIn, cOut, cRules, cCat, cCreated, - C.uint8_t(opts.MinZoom), C.uint8_t(opts.MaxZoom), cOmit(opts.OmitPickAttrs), C.uint8_t(opts.Format), user, &cells, &bb[0]) - switch rc { - case 1: - return int(cells), [4]float64{float64(bb[0]), float64(bb[1]), float64(bb[2]), float64(bb[3])}, nil - case 0: - return 0, bbox, fmt.Errorf("tile57: bundle bake covered nothing: %w", ErrNoCoverage) - default: - return 0, bbox, fmt.Errorf("tile57: bundle bake failed") + if charts == 0 { + return 0, fmt.Errorf("tile57: partition-debug bake covered nothing: %w", ErrNoCoverage) } + return int(charts), nil } diff --git a/bindings/go/bundle_test.go b/bindings/go/bundle_test.go index 17bc085..2725907 100644 --- a/bindings/go/bundle_test.go +++ b/bindings/go/bundle_test.go @@ -3,17 +3,11 @@ package tile57 import ( - "os" - "path/filepath" "testing" ) func TestSourceScamin(t *testing.T) { - data, err := os.ReadFile(testCell) - if err != nil { - t.Skipf("no test cell: %v", err) - } - src, err := OpenChartBytes(data) + src, err := OpenBytes(bakeTestCell(t)) if err != nil { t.Fatal(err) } @@ -33,48 +27,3 @@ func TestSourceScamin(t *testing.T) { } t.Logf("SCAMIN manifest: %v", sc) } - -func TestBakeBundle(t *testing.T) { - if _, err := os.Stat(testCell); err != nil { - t.Skipf("no test cell: %v", err) - } - out := t.TempDir() - // Capture the progress reports to verify the band label detail (host §3): a - // stage-1 report must carry a band name + a sane band index/count. - var stage1Named bool - var maxBandCount int - progress := func(p BakeProgress) { - if p.BandCount > maxBandCount { - maxBandCount = p.BandCount - } - if p.Stage == 1 && p.BandName != "" && p.BandIndex < p.BandCount { - stage1Named = true - } - } - n, bbox, err := BakeBundle(testCell, out, BakeOpts{MaxZoom: 16}, progress) - if err != nil { - t.Fatal(err) - } - if n == 0 { - t.Fatal("baked 0 cells") - } - if maxBandCount == 0 { - t.Fatal("progress never reported a band count") - } - if !stage1Named { - t.Fatal("progress never reported a stage-1 band label (name + index embed -> read -> decode. +func TestBakeCellEmbedsCoverage(t *testing.T) { + if _, err := os.Stat(testCell); err != nil { + t.Skipf("no test cell %s: %v", testCell, err) + } + + pm, err := BakeChart(testCell) + if err != nil { + t.Fatalf("BakeChart: %v", err) + } + if len(pm) < 7 || string(pm[:7]) != "PMTiles" { + t.Fatalf("not a PMTiles archive (got %d bytes)", len(pm)) + } + + meta, err := PMTilesMetadata(pm) + if err != nil { + t.Fatalf("PMTilesMetadata: %v", err) + } + if meta == nil { + t.Fatal("archive carries no metadata") + } + + var m struct { + Coverage *struct { + Name string `json:"name"` + Date string `json:"date"` + Cscl int32 `json:"cscl"` + Band uint8 `json:"band"` + Bbox [4]int32 `json:"bbox"` + Cov1 [][][][2]int32 `json:"cov1"` + } `json:"coverage"` + } + if err := json.Unmarshal(meta, &m); err != nil { + t.Fatalf("unmarshal metadata: %v\n%s", err, meta) + } + if m.Coverage == nil { + t.Fatal("no coverage embedded in the metadata") + } + c := m.Coverage + if c.Name != "US5MD1MC" { + t.Errorf("coverage name = %q, want US5MD1MC", c.Name) + } + if c.Cscl <= 0 { + t.Errorf("coverage cscl = %d, want > 0", c.Cscl) + } + if len(c.Cov1) == 0 { + t.Fatal("no M_COVR coverage rings") + } + // bbox must be a non-degenerate box in integer lon/lat. + if !(c.Bbox[0] < c.Bbox[2] && c.Bbox[1] < c.Bbox[3]) { + t.Errorf("degenerate bbox %v", c.Bbox) + } + nPts := 0 + for _, feat := range c.Cov1 { + for _, ring := range feat { + nPts += len(ring) + } + } + if nPts < 3 { + t.Errorf("coverage has %d points, want a real ring (>= 3)", nPts) + } + // Every vertex must sit within the reported bbox (integer coords, no f64 drift). + for _, feat := range c.Cov1 { + for _, ring := range feat { + for _, p := range ring { + if p[0] < c.Bbox[0] || p[0] > c.Bbox[2] || p[1] < c.Bbox[1] || p[1] > c.Bbox[3] { + t.Fatalf("vertex %v outside bbox %v", p, c.Bbox) + } + } + } + } + t.Logf("US5MD1MC: cscl=%d band=%d date=%q, %d M_COVR feature(s), %d pts, bbox=%v", + c.Cscl, c.Band, c.Date, len(c.Cov1), nPts, c.Bbox) +} diff --git a/bindings/go/cmem.go b/bindings/go/cmem.go index bfe4f18..42e2693 100644 --- a/bindings/go/cmem.go +++ b/bindings/go/cmem.go @@ -46,18 +46,9 @@ func (a *cArena) str(s string) *C.char { return (*C.char)(a.track(unsafe.Pointer(C.CString(s)))) } -// cOmit maps the Go BakeOpts.OmitPickAttrs bool to the C omit_pick_attrs int: 0 = -// include the per-feature pick attrs (the default), 1 = omit them. -func cOmit(omit bool) C.int { - if omit { - return 1 - } - return 0 -} - // int32Array copies a Go []int32 into a malloc'd C int32_t array, returning the // base pointer + count (NULL/0 for an empty slice). Used for the enabled-bands and -// SCAMIN-manifest inputs to tile57_build_style. +// SCAMIN-manifest inputs to tile57_style_build. func (a *cArena) int32Array(v []int32) (*C.int32_t, C.size_t) { n := len(v) if n == 0 { @@ -71,36 +62,3 @@ func (a *cArena) int32Array(v []int32) (*C.int32_t, C.size_t) { return p, C.size_t(n) } -// cellInputs builds a C tile57_cell array from cells. Returns the typed Go view -// (for length bookkeeping) and the base pointer to hand to C. -func (a *cArena) cellInputs(cells []Cell) ([]C.tile57_cell, *C.tile57_cell) { - n := len(cells) - base := (*C.tile57_cell)(a.track(C.malloc(C.size_t(n) * C.size_t(unsafe.Sizeof(C.tile57_cell{}))))) - inputs := unsafe.Slice(base, n) - for i, c := range cells { - in := &inputs[i] - in.base = a.bytes(c.Base) - in.base_len = C.size_t(len(c.Base)) - in.updates = nil - in.update_lens = nil - in.update_count = 0 - in.name = nil - if c.Name != "" { - in.name = a.str(c.Name) - } - if m := len(c.Updates); m > 0 { - updPtrs := (**C.uint8_t)(a.track(C.malloc(C.size_t(m) * C.size_t(unsafe.Sizeof((*C.uint8_t)(nil)))))) - updLens := (*C.size_t)(a.track(C.malloc(C.size_t(m) * C.size_t(unsafe.Sizeof(C.size_t(0)))))) - ps := unsafe.Slice(updPtrs, m) - ls := unsafe.Slice(updLens, m) - for j, u := range c.Updates { - ps[j] = a.bytes(u) - ls[j] = C.size_t(len(u)) - } - in.updates = updPtrs - in.update_lens = updLens - in.update_count = C.size_t(m) - } - } - return inputs, base -} diff --git a/bindings/go/compose.go b/bindings/go/compose.go new file mode 100644 index 0000000..effddb1 --- /dev/null +++ b/bindings/go/compose.go @@ -0,0 +1,183 @@ +//go:build cgo + +package tile57 + +/* +#include +#include "tile57.h" +*/ +import "C" + +import ( + "fmt" + "sync" + "unsafe" +) + +// ComposeSource is a resident runtime compositor over open charts: the ownership +// partition loaded once, so Tile composes any tile on demand. It BORROWS its +// charts — they must outlive it — except charts opened for it by [OpenCompose], +// which it owns and closes. Tile is serialised by an internal mutex (the +// compositor reads through the charts, which are not thread-safe). +type ComposeSource struct { + mu sync.Mutex + ptr *C.tile57_compose + owned []*Source // charts OpenCompose opened for us; closed on Close +} + +// ComposeMeta is a ComposeSource's served zoom range, coverage-carrying chart +// count, and union coverage bounds (degrees). +type ComposeMeta struct { + MinZoom, MaxZoom uint8 + Charts uint32 + West, South, East, North float64 +} + +// OpenComposeCharts opens a resident compositor over already-open charts, +// BORROWING them: every chart must outlive the compositor (Close the compositor +// first), and while it serves, don't call those charts' own methods from other +// goroutines. Charts whose archives embed no coverage are skipped; if none +// carries coverage the open fails with ErrNoCoverage. If partitionPath is +// non-empty it names a partition sidecar (from [ComposeSource.SavePartition]; the +// `tile57 bake` CLI writes partition.tpart) to load and skip the owned-face +// build; a missing/stale one falls back to building. +func OpenComposeCharts(charts []*Source, partitionPath string) (*ComposeSource, error) { + if len(charts) == 0 { + return nil, fmt.Errorf("tile57: OpenComposeCharts needs at least one chart: %w", ErrEmptyInput) + } + var ar cArena + defer ar.free() + // A C array of chart handles — pointer-free from Go's view (cgocheck-safe). + cc := (**C.tile57_chart)(ar.track(C.malloc(C.size_t(len(charts)) * C.size_t(unsafe.Sizeof((*C.tile57_chart)(nil)))))) + cv := unsafe.Slice(cc, len(charts)) + for i, s := range charts { + if s == nil || s.ptr == nil { + return nil, fmt.Errorf("tile57: OpenComposeCharts: chart %d is nil/closed: %w", i, ErrEmptyInput) + } + cv[i] = s.ptr + } + var cpart *C.char + if partitionPath != "" { + cpart = ar.str(partitionPath) + } + var ptr *C.tile57_compose + var cerr C.tile57_error + if st := C.tile57_compose_open(cc, C.size_t(len(charts)), cpart, &ptr, &cerr); st != C.TILE57_OK { + // "No coverage-carrying chart" is TILE57_ERR_UNSUPPORTED; surface it as + // ErrNoCoverage so a host can branch, keeping the specific message. + if st == C.TILE57_ERR_UNSUPPORTED { + return nil, fmt.Errorf("%s: %w", C.GoString(&cerr.message[0]), ErrNoCoverage) + } + return nil, statusError(st, &cerr) + } + return &ComposeSource{ptr: ptr}, nil +} + +// OpenCompose opens a resident compositor over the per-chart PMTiles at paths +// (each written by [BakeChart] / [BakeTree]): every path is opened as a chart +// (mmap'd, so the chart set is never fully resident) and the compositor OWNS those charts — +// Close releases them too. See [OpenComposeCharts] to compose over charts you +// keep. partitionPath as in OpenComposeCharts. +func OpenCompose(paths []string, partitionPath string) (*ComposeSource, error) { + if len(paths) == 0 { + return nil, fmt.Errorf("tile57: OpenCompose needs at least one path: %w", ErrEmptyInput) + } + charts := make([]*Source, 0, len(paths)) + closeAll := func() { + for _, s := range charts { + _ = s.Close() + } + } + for _, p := range paths { + s, err := Open(p) + if err != nil { + closeAll() + return nil, fmt.Errorf("tile57: OpenCompose: %w", err) + } + charts = append(charts, s) + } + cs, err := OpenComposeCharts(charts, partitionPath) + if err != nil { + closeAll() + return nil, err + } + cs.owned = charts + return cs, nil +} + +// Tile composes the tile (z,x,y) on demand, returning raw (decompressed) MLT bytes plus `owned` — +// whether the ownership partition says a chart SHOULD render here. body!=nil → composed (owned). +// body==nil && owned → a chart owns this ground but produced nothing (transient while its per-chart +// bake runs; an error state once bakes are done). body==nil && !owned → true empty ocean (safe to cache). +// Thread-safe (serialised). +func (c *ComposeSource) Tile(z uint8, x, y uint32) (body []byte, owned bool, err error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.ptr == nil { + return nil, false, fmt.Errorf("tile57: Tile on a closed ComposeSource") + } + var out *C.uint8_t + var outLen C.size_t + var cowned C.bool + var cerr C.tile57_error + if st := C.tile57_compose_tile(c.ptr, C.uint8_t(z), C.uint32_t(x), C.uint32_t(y), &out, &outLen, &cowned, &cerr); st != C.TILE57_OK { + return nil, false, statusError(st, &cerr) + } + if out == nil { + return nil, bool(cowned), nil + } + return tileBytes(out, outLen), true, nil +} + +// Meta returns the compositor's served zoom range + union coverage bounds. +func (c *ComposeSource) Meta() ComposeMeta { + c.mu.Lock() + defer c.mu.Unlock() + if c.ptr == nil { + return ComposeMeta{} + } + var m C.tile57_compose_meta + C.tile57_compose_get_meta(c.ptr, &m) + return ComposeMeta{ + MinZoom: uint8(m.min_zoom), + MaxZoom: uint8(m.max_zoom), + Charts: uint32(m.charts), + West: float64(m.west), + South: float64(m.south), + East: float64(m.east), + North: float64(m.north), + } +} + +// SavePartition serializes the resident ownership partition to path — a sidecar a later +// compose open can load (as partitionPath) to skip the owned-face build. +func (c *ComposeSource) SavePartition(path string) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.ptr == nil { + return fmt.Errorf("tile57: SavePartition on a closed ComposeSource") + } + var ar cArena + defer ar.free() + var cerr C.tile57_error + if st := C.tile57_compose_save_partition(c.ptr, ar.str(path), &cerr); st != C.TILE57_OK { + return statusError(st, &cerr) + } + return nil +} + +// Close releases the compositor, then any charts [OpenCompose] opened for it. +// Borrowed charts (from [OpenComposeCharts]) stay open. Idempotent. +func (c *ComposeSource) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.ptr != nil { + C.tile57_compose_close(c.ptr) + c.ptr = nil + } + for _, s := range c.owned { + _ = s.Close() + } + c.owned = nil + return nil +} diff --git a/bindings/go/compose_test.go b/bindings/go/compose_test.go new file mode 100644 index 0000000..a1ca460 --- /dev/null +++ b/bindings/go/compose_test.go @@ -0,0 +1,147 @@ +//go:build cgo + +package tile57 + +import ( + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +// lonLatToTile lives in tile57_test.go (same package). + +// The whole pipeline over the in-repo fixture: bake the cell, open it as a chart, +// compose over the chart (borrowed), serve the coverage-centre tile, and round-trip +// the partition sidecar through a path-based (owning) open. +func TestComposeSingleCell(t *testing.T) { + pm := bakeTestCell(t) + path := filepath.Join(t.TempDir(), "US5MD1MC.pmtiles") + if err := os.WriteFile(path, pm, 0o644); err != nil { + t.Fatal(err) + } + chart, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer chart.Close() + + src, err := OpenComposeCharts([]*Source{chart}, "") + if err != nil { + t.Fatalf("OpenComposeCharts: %v", err) + } + m := src.Meta() + if m.Charts != 1 { + t.Fatalf("compose charts = %d, want 1", m.Charts) + } + // The fill-up bake serves from z0, but a single harbour cell is sub-pixel at + // world zooms (its low-zoom tiles are legitimately empty) — probe a zoom + // where the cell has real extent. + z := uint8(10) + if z < m.MinZoom { + z = m.MinZoom + } + cx, cy := lonLatToTile((m.West+m.East)/2, (m.South+m.North)/2, z) + tile, owned, err := src.Tile(z, cx, cy) + if err != nil { + t.Fatalf("Tile: %v", err) + } + if len(tile) == 0 || !owned { + t.Fatalf("centre tile z%d/%d/%d: %d bytes owned=%v, want content", z, cx, cy, len(tile), owned) + } + part := filepath.Join(t.TempDir(), "partition.tpart") + if err := src.SavePartition(part); err != nil { + t.Fatalf("SavePartition: %v", err) + } + if err := src.Close(); err != nil { + t.Fatal(err) + } + + // Path-based open owns its charts and loads the sidecar. + src2, err := OpenCompose([]string{path}, part) + if err != nil { + t.Fatalf("OpenCompose: %v", err) + } + defer src2.Close() + tile2, owned2, err := src2.Tile(z, cx, cy) + if err != nil { + t.Fatalf("Tile(sidecar): %v", err) + } + if !owned2 || len(tile2) != len(tile) { + t.Fatalf("sidecar-loaded serve differs: %d bytes owned=%v (want %d bytes)", len(tile2), owned2, len(tile)) + } + t.Logf("compose z%d..%d served %d bytes at z%d/%d/%d (sidecar round-trip ok)", + m.MinZoom, m.MaxZoom, len(tile), z, cx, cy) +} + +// Set TILE57_COMPOSE_TESTDIR to a dir of per-cell *.cell.tmp/*.pmtiles archives (e.g. from +// `tile57 compose -o out.pmtiles --keep-cells`), optionally TILE57_COMPOSE_PARTITION to +// a sidecar (`--save-partition`). Skips when unset — no machine paths baked into the test. +func TestOpenComposeServe(t *testing.T) { + dir := os.Getenv("TILE57_COMPOSE_TESTDIR") + if dir == "" { + t.Skip("set TILE57_COMPOSE_TESTDIR to a dir of per-cell *.cell.tmp/*.pmtiles archives") + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + var paths []string + for _, e := range entries { + if e.IsDir() { + continue + } + if n := e.Name(); strings.HasSuffix(n, ".cell.tmp") || strings.HasSuffix(n, ".pmtiles") { + paths = append(paths, filepath.Join(dir, n)) + } + } + sort.Strings(paths) + if len(paths) == 0 { + t.Fatalf("no per-cell archives in %s", dir) + } + + src, err := OpenCompose(paths, os.Getenv("TILE57_COMPOSE_PARTITION")) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + m := src.Meta() + t.Logf("compose: %d charts, z%d..%d, bounds [%.3f, %.3f, %.3f, %.3f]", + m.Charts, m.MinZoom, m.MaxZoom, m.West, m.South, m.East, m.North) + if m.Charts == 0 { + t.Fatal("no coverage-carrying charts") + } + + // Serve the tile at the coverage centre, a few zooms into the range — the centre of real + // coverage should have content, so a non-blank tile proves the serve path end-to-end. + z := m.MinZoom + 3 + if z > m.MaxZoom { + z = m.MaxZoom + } + cx, cy := lonLatToTile((m.West+m.East)/2, (m.South+m.North)/2, z) + tile, owned, err := src.Tile(z, cx, cy) + if err != nil { + t.Fatal(err) + } + t.Logf("served z%d/%d/%d: %d bytes (raw MLT), owned=%v", z, cx, cy, len(tile), owned) + if len(tile) == 0 { + t.Fatalf("centre tile z%d/%d/%d is blank — expected content", z, cx, cy) + } + if !owned { + t.Fatalf("centre tile z%d/%d/%d has content but owned=false", z, cx, cy) + } + + // A tile far outside coverage must be blank (nil) AND not owned (true empty ocean), not an error. + blank, blankOwned, err := src.Tile(z, 0, 0) + if err != nil { + t.Fatal(err) + } + if blank != nil { + t.Fatalf("tile z%d/0/0 should be blank, got %d bytes", z, len(blank)) + } + if blankOwned { + t.Fatalf("tile z%d/0/0 (far outside coverage) should be unowned", z) + } +} diff --git a/bindings/go/coverage.go b/bindings/go/coverage.go new file mode 100644 index 0000000..f054d2f --- /dev/null +++ b/bindings/go/coverage.go @@ -0,0 +1,57 @@ +//go:build cgo + +package tile57 + +/* +#include "tile57.h" + +// Trampoline: the C ring callback calls back into this exported Go function. +// (cgo exports can't carry const, so the helper casts to the header's type.) +extern void tile57GoCoverageRing(void *ctx, double *lonlat, size_t npts); + +// Build the callback table C-side so no C function pointer crosses into Go. +static tile57_status t57CoverageGo(tile57_chart *chart, void *handle, tile57_error *err) { + tile57_coverage_cb cb = {handle, (void (*)(void *, const double *, size_t))tile57GoCoverageRing}; + return tile57_chart_coverage(chart, &cb, err); +} +*/ +import "C" + +import ( + "runtime/cgo" + "unsafe" +) + +//export tile57GoCoverageRing +func tile57GoCoverageRing(ctx unsafe.Pointer, lonlat *C.double, npts C.size_t) { + rings, ok := cgo.Handle(uintptr(ctx)).Value().(*[][][2]float64) + if !ok || npts == 0 { + return + } + pts := unsafe.Slice(lonlat, int(npts)*2) + ring := make([][2]float64, int(npts)) + for i := range ring { + ring[i] = [2]float64{float64(pts[2*i]), float64(pts[2*i+1])} + } + *rings = append(*rings, ring) +} + +// Coverage returns the chart's M_COVR data-coverage polygons (one exterior ring +// per polygon, lon/lat points) from the coverage the bake embedded in the archive +// metadata — the real coverage a host reports so a quilt fills gaps to coarser +// charts. Nil when the archive embeds none (a composed/foreign archive). +func (s *Source) Coverage() ([][][2]float64, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.ptr == nil { + return nil, ErrSourceClosed + } + var rings [][][2]float64 + h := cgo.NewHandle(&rings) + defer h.Delete() + var cerr C.tile57_error + if st := C.t57CoverageGo(s.ptr, unsafe.Pointer(h), &cerr); st != C.TILE57_OK { //nolint:govet // cgo.Handle as void* ctx + return nil, statusError(st, &cerr) + } + return rings, nil +} diff --git a/bindings/go/errors.go b/bindings/go/errors.go index b0ad0c6..c92c732 100644 --- a/bindings/go/errors.go +++ b/bindings/go/errors.go @@ -16,7 +16,18 @@ var ( // ErrSourceClosed is returned by Source methods invoked after Close. ErrSourceClosed = errors.New("tile57: source closed") - // ErrEmptyInput is returned when a call is handed no usable input — no cells, + // ErrEmptyInput is returned when a call is handed no usable input — no charts, // empty bytes, an empty style template, or empty asset inputs. ErrEmptyInput = errors.New("tile57: empty input") + + // Category sentinels matching the C tile57_status codes. A call that fails + // wraps the matching sentinel with %w (the message stays specific — often + // "path: reason"), so a host can branch with errors.Is(err, ErrParse) etc. + ErrBadArg = errors.New("tile57: invalid argument") + ErrIO = errors.New("tile57: I/O error") + ErrParse = errors.New("tile57: malformed input") + ErrNoMem = errors.New("tile57: out of memory") + ErrUnsupported = errors.New("tile57: unsupported input") + ErrRender = errors.New("tile57: render failed") + ErrInternal = errors.New("tile57: internal error") ) diff --git a/bindings/go/errors_test.go b/bindings/go/errors_test.go index 9e402e1..182b8a5 100644 --- a/bindings/go/errors_test.go +++ b/bindings/go/errors_test.go @@ -11,12 +11,17 @@ import ( // host can branch on "empty input" / "covered nothing" / "source closed" rather than // scraping the message string. func TestSentinelErrors(t *testing.T) { - // ErrEmptyInput: no cells. - if _, err := BakePmtiles(nil, BakeOpts{}, nil); !errors.Is(err, ErrEmptyInput) { - t.Errorf("BakePmtiles(nil): want ErrEmptyInput, got %v", err) + // ErrEmptyInput: empty input. + if _, err := OpenBytes(nil); !errors.Is(err, ErrEmptyInput) { + t.Errorf("OpenBytes(nil): want ErrEmptyInput, got %v", err) } - if _, err := OpenChartBytes(nil); !errors.Is(err, ErrEmptyInput) { - t.Errorf("OpenChartBytes(nil): want ErrEmptyInput, got %v", err) + // Category sentinels ride the C status: a missing file is ErrIO, garbage + // archive bytes are ErrParse. + if _, err := Open("testdata/definitely-missing.pmtiles"); !errors.Is(err, ErrIO) { + t.Errorf("Open(missing): want ErrIO, got %v", err) + } + if _, err := OpenBytes([]byte("not a pmtiles archive")); !errors.Is(err, ErrParse) { + t.Errorf("OpenBytes(garbage): want ErrParse, got %v", err) } if _, err := Open(""); !errors.Is(err, ErrEmptyInput) { t.Errorf("Open(\"\"): want ErrEmptyInput, got %v", err) @@ -24,10 +29,4 @@ func TestSentinelErrors(t *testing.T) { if _, err := BuildStyle(nil, MarinerDefaults(), nil, nil, nil, 0); !errors.Is(err, ErrEmptyInput) { t.Errorf("BuildStyle(empty template): want ErrEmptyInput, got %v", err) } - - // ErrSourceClosed: Tile on a closed source. - s := &Source{} // ptr nil == closed - if _, err := s.Tile(0, 0, 0); !errors.Is(err, ErrSourceClosed) { - t.Errorf("Tile on closed source: want ErrSourceClosed, got %v", err) - } } diff --git a/bindings/go/partition_test.go b/bindings/go/partition_test.go new file mode 100644 index 0000000..684fe7b --- /dev/null +++ b/bindings/go/partition_test.go @@ -0,0 +1,42 @@ +//go:build cgo + +package tile57 + +import ( + "os" + "path/filepath" + "testing" +) + +func TestBakePartitionDebug(t *testing.T) { + if _, err := os.Stat(testCell); err != nil { + t.Skipf("no test cell: %v", err) + } + // testCell lives in testdata/, so that directory is a one-cell ENC_ROOT. + out := filepath.Join(t.TempDir(), "partition.pmtiles") + n, err := BakePartitionDebug("testdata", out, 0, 9, BandGoverning) + if err != nil { + t.Fatal(err) + } + if n == 0 { + t.Fatal("baked 0 charts") + } + fi, err := os.Stat(out) + if err != nil || fi.Size() == 0 { + t.Fatalf("partition pmtiles missing/empty: %v", err) + } + b, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + if len(b) < 7 || string(b[:7]) != "PMTiles" { + t.Fatalf("output is not a PMTiles archive (first bytes %q)", b[:min(7, len(b))]) + } + + // A single band's own map bakes too (harbor = the finest partition). + outBand := filepath.Join(t.TempDir(), "harbor.pmtiles") + if _, err := BakePartitionDebug("testdata", outBand, 0, 11, BandHarbor); err != nil { + t.Fatalf("per-band bake failed: %v", err) + } + t.Logf("partition-debug: %d cell(s) -> %d bytes", n, fi.Size()) +} diff --git a/bindings/go/progress.go b/bindings/go/progress.go deleted file mode 100644 index 583a405..0000000 --- a/bindings/go/progress.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build cgo - -package tile57 - -/* -#include "tile57.h" -*/ -import "C" - -import ( - "runtime/cgo" - "unsafe" -) - -// BakeProgress is one progress report from [BakePmtiles] / [BakeBundle]. -type BakeProgress struct { - Stage uint8 // 0 = loading/portraying cells, 1 = baking tiles - Done int // cells loaded (stage 0) or tiles baked in this band (stage 1) - Total int // total cells (stage 0) or the band's planned tile count (stage 1; 0 = unknown) - // BandIndex/BandCount locate the current band among the bands that actually bake - // (0-based index; count = how many bands have cells), so a UI can show "band i/n". - BandIndex int - BandCount int - // BandName is the navigational-purpose name of the current band ("berthing", - // "harbor", "approach", "coastal", "general", "overview"), or "" if not band-specific. - BandName string -} - -// tile57GoBakeProgress is the C-callable progress callback libtile57 invokes -// during tile57_bake_pmtiles / tile57_bake_bundle. The host's Go progress func is -// carried across the seam as a cgo.Handle, pointed to by `user`. NULL user = no -// progress wanted. -// -//export tile57GoBakeProgress -func tile57GoBakeProgress(user unsafe.Pointer, stage C.uint8_t, done, total C.size_t, bandIndex, bandCount C.uint8_t, bandName *C.char) { - if user == nil { - return - } - h := *(*cgo.Handle)(user) - if cb, ok := h.Value().(func(BakeProgress)); ok && cb != nil { - name := "" - if bandName != nil { - name = C.GoString(bandName) - } - cb(BakeProgress{ - Stage: uint8(stage), - Done: int(done), - Total: int(total), - BandIndex: int(bandIndex), - BandCount: int(bandCount), - BandName: name, - }) - } -} diff --git a/bindings/go/s57meta.go b/bindings/go/s57meta.go index c999043..ef90f92 100644 --- a/bindings/go/s57meta.go +++ b/bindings/go/s57meta.go @@ -2,8 +2,8 @@ package tile57 -// Chart metadata + raw S-57 access: per-cell metadata, exchange-set catalogue -// decode, and raw feature access — everything a host previously parsed from +// Raw S-57 access, handle-free: per-chart metadata, GeoJSON feature extraction, +// and exchange-set catalogue decode — everything a host previously parsed from // S-57/ISO-8211 itself. With these, a host's S-57 knowledge shrinks to file // staging conventions (.000/.NNN, ENC_ROOT/, CATALOG.031 as opaque bytes). @@ -20,10 +20,10 @@ import ( "unsafe" ) -// CellInfo is one cell's identity + coverage, as recorded in its DSID/DSPM +// ChartRecord is one chart's identity + coverage, as recorded in its DSID/DSPM // after the applied update chain. -type CellInfo struct { - Name string `json:"name"` // cell stem, e.g. "US5MD1MC" +type ChartRecord struct { + Name string `json:"name"` // chart stem, e.g. "US5MD1MC" Scale int `json:"scale"` // DSPM CSCL (1:N) Edition string `json:"edition"` // DSID EDTN Update string `json:"update"` // DSID UPDN (last applied update) @@ -33,35 +33,35 @@ type CellInfo struct { HasBBox bool `json:"-"` } -// Cells returns the chart's per-cell metadata (one entry per cell, DSID fields -// after the update chain). A PMTiles source returns nil — its bundle manifest -// carries the cell inventory. -func (s *Source) Cells() ([]CellInfo, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.ptr == nil { - return nil, ErrSourceClosed - } +// Charts returns the per-chart metadata of the S-57 data at path — one .000 file +// (with its update chain applied) or a whole ENC_ROOT directory — for a host's +// chart-database scan. +func Charts(path string) ([]ChartRecord, error) { + if path == "" { + return nil, fmt.Errorf("tile57: empty path: %w", ErrEmptyInput) + } + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) var out *C.uint8_t var outLen C.size_t - switch C.tile57_chart_cells(s.ptr, &out, &outLen) { - case 1: - case 0: + var cerr C.tile57_error + if st := C.tile57_enc_charts(cPath, &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) + } + if out == nil { return nil, nil - default: - return nil, fmt.Errorf("tile57: cell metadata error") } raw := tileBytes(out, outLen) var wire []struct { - CellInfo + ChartRecord BBox []float64 `json:"bbox"` } if err := json.Unmarshal(raw, &wire); err != nil { - return nil, fmt.Errorf("tile57: cell metadata decode: %w", err) + return nil, fmt.Errorf("tile57: chart metadata decode: %w", err) } - infos := make([]CellInfo, len(wire)) + infos := make([]ChartRecord, len(wire)) for i, w := range wire { - infos[i] = w.CellInfo + infos[i] = w.ChartRecord if len(w.BBox) == 4 { copy(infos[i].BBox[:], w.BBox) infos[i].HasBBox = true @@ -74,7 +74,7 @@ func (s *Source) Cells() ([]CellInfo, error) { type CatalogEntry struct { File string `json:"file"` // recorded path, '/'-normalised LongName string `json:"longName"` // LFIL — the human chart title ("" when absent) - Impl string `json:"impl"` // "BIN" (a cell), "ASC", "TXT" + Impl string `json:"impl"` // "BIN" (a chart), "ASC", "TXT" BBox [4]float64 `json:"bbox"` // [west, south, east, north] HasBBox bool `json:"-"` } @@ -87,12 +87,12 @@ func CatalogEntries(catalog []byte) ([]CatalogEntry, error) { } var out *C.uint8_t var outLen C.size_t - switch C.tile57_catalog_entries((*C.uint8_t)(unsafe.Pointer(&catalog[0])), C.size_t(len(catalog)), &out, &outLen) { - case 1: - case 0: + var cerr C.tile57_error + if st := C.tile57_enc_catalog((*C.uint8_t)(unsafe.Pointer(&catalog[0])), C.size_t(len(catalog)), &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) + } + if out == nil { return nil, nil - default: - return nil, fmt.Errorf("tile57: catalogue parse error") } raw := tileBytes(out, outLen) var wire []struct { @@ -113,8 +113,8 @@ func CatalogEntries(catalog []byte) ([]CatalogEntry, error) { return entries, nil } -// Feature is one S-57 feature from [Source.Features]: its class acronym, the -// full attribute map (acronym → raw string value), and GeoJSON geometry. +// Feature is one S-57 feature from [Features]: its class acronym, the full +// attribute map (acronym → raw string value), and GeoJSON geometry. type Feature struct { Class string // e.g. "DEPARE" Attrs map[string]string // full S-57 attribute set, e.g. {"DRVAL1":"3.6"} @@ -122,28 +122,55 @@ type Feature struct { Geometry json.RawMessage // the GeoJSON geometry object (lon/lat; soundings carry depth as a 3rd coord) } -// Features returns the chart's features for the given object-class acronyms -// (e.g. "DEPARE", "DRGARE"), parsed without portrayal. A whole-ENC_ROOT query -// walks every cell — the caller owns that cost. -func (s *Source) Features(classes ...string) ([]Feature, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.ptr == nil { - return nil, ErrSourceClosed +// Features returns the features of the S-57 data at path (one chart, updates +// applied, or a whole ENC_ROOT) for the given object-class acronyms (e.g. +// "DEPARE", "DRGARE"), parsed without portrayal. A whole-ENC_ROOT extraction +// walks every chart — the caller owns that cost. +func Features(path string, classes ...string) ([]Feature, error) { + if path == "" { + return nil, fmt.Errorf("tile57: empty path: %w", ErrEmptyInput) } if len(classes) == 0 { return nil, nil } + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) cs := C.CString(strings.Join(classes, ",")) defer C.free(unsafe.Pointer(cs)) var out *C.uint8_t var outLen C.size_t - switch C.tile57_chart_features(s.ptr, cs, &out, &outLen) { - case 1: - case 0: + var cerr C.tile57_error + if st := C.tile57_enc_features(cPath, cs, &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) + } + return decodeFeatures(out, outLen) +} + +// FeaturesBytes is [Features] over in-memory base .000 bytes (read from a +// zip member, say). No update chain is applied. +func FeaturesBytes(base []byte, classes ...string) ([]Feature, error) { + if len(base) == 0 { + return nil, fmt.Errorf("tile57: empty chart bytes: %w", ErrEmptyInput) + } + if len(classes) == 0 { + return nil, nil + } + cs := C.CString(strings.Join(classes, ",")) + defer C.free(unsafe.Pointer(cs)) + var out *C.uint8_t + var outLen C.size_t + var cerr C.tile57_error + if st := C.tile57_enc_features_bytes((*C.uint8_t)(unsafe.Pointer(&base[0])), C.size_t(len(base)), cs, &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) + } + return decodeFeatures(out, outLen) +} + +// decodeFeatures unmarshals an engine GeoJSON FeatureCollection buffer (freeing +// it) into []Feature. A nil buffer (nothing matched) decodes to nil. +func decodeFeatures(out *C.uint8_t, outLen C.size_t) ([]Feature, error) { + if out == nil { return nil, nil - default: - return nil, fmt.Errorf("tile57: feature query error") } raw := tileBytes(out, outLen) var fc struct { diff --git a/bindings/go/s57meta_test.go b/bindings/go/s57meta_test.go index 304ee09..984f50c 100644 --- a/bindings/go/s57meta_test.go +++ b/bindings/go/s57meta_test.go @@ -4,26 +4,21 @@ package tile57 import ( "encoding/json" + "os" "testing" ) -// TestCells reads the testdata cell's DSID identity + coverage through the -// chart handle — the metadata a host previously parsed from ISO-8211 itself. +// TestCells reads the testdata cell's DSID identity + coverage handle-free — +// the metadata a host previously parsed from ISO-8211 itself. func TestCells(t *testing.T) { - src, err := Open(testCell) + charts, err := Charts(testCell) if err != nil { - t.Fatalf("Open: %v", err) + t.Fatalf("Charts: %v", err) } - defer src.Close() - - cells, err := src.Cells() - if err != nil { - t.Fatalf("Cells: %v", err) - } - if len(cells) != 1 { - t.Fatalf("cells = %d, want 1", len(cells)) + if len(charts) != 1 { + t.Fatalf("charts = %d, want 1", len(charts)) } - c := cells[0] + c := charts[0] if c.Name != "US5MD1MC" { t.Errorf("name = %q, want US5MD1MC", c.Name) } @@ -44,15 +39,21 @@ func TestCells(t *testing.T) { // TestFeatures runs the NMEA-simulator water-mask query: DEPARE/DRGARE // polygons with DRVAL1, closed rings, lon/lat coordinates. func TestFeatures(t *testing.T) { - src, err := Open(testCell) + feats, err := Features(testCell, "DEPARE", "DRGARE") if err != nil { - t.Fatalf("Open: %v", err) + t.Fatalf("Features: %v", err) } - defer src.Close() - - feats, err := src.Features("DEPARE", "DRGARE") + // The bytes variant (the NMEA simulator's zip-member path) must agree. + data, err := os.ReadFile(testCell) if err != nil { - t.Fatalf("Features: %v", err) + t.Fatal(err) + } + bfeats, err := FeaturesBytes(data, "DEPARE", "DRGARE") + if err != nil { + t.Fatalf("FeaturesBytes: %v", err) + } + if len(bfeats) == 0 { + t.Fatal("FeaturesBytes returned nothing") } if len(feats) == 0 { t.Fatal("no DEPARE/DRGARE features") diff --git a/bindings/go/style.go b/bindings/go/style.go index fd31f0a..fe12941 100644 --- a/bindings/go/style.go +++ b/bindings/go/style.go @@ -66,7 +66,7 @@ type Mariner struct { DateDependent, HighlightDateDependent bool DateView string // "YYYYMMDD" or "" (today) IgnoreScamin bool // ?ignoreScamin: drop SCAMIN gating, show all in-band - ScaminFilterGate bool // scamin-layers.md: one live-filtered *_scamin layer per render-type instead of per-value buckets + ScaminFilterGate bool // one live-filtered *_scamin layer per render-type instead of per-value buckets ShowOverscale bool // S-52 §10.1.10 AP(OVERSC01) overscale indication (default true) SizeScale float64 // physical-scale multiplier for icon/line/text sizes (1.0 = verbatim) ViewingGroupsOff []int32 // S-52 §14.5 DENY-LIST: vg ids turned OFF (nil/empty = show all) @@ -99,9 +99,10 @@ func StyleTemplate(scheme Scheme, sourceTiles, sprite, glyphs string, minZoom, m defer f3() var out *C.uint8_t var n C.size_t - if C.tile57_style_template(C.tile57_scheme(scheme), cSrc, cSpr, cGly, - C.uint32_t(minZoom), C.uint32_t(maxZoom), C.uint8_t(encoding), &out, &n) != 1 { - return nil, fmt.Errorf("tile57: style_template failed") + var cerr C.tile57_error + if st := C.tile57_style_template(C.tile57_scheme(scheme), cSrc, cSpr, cGly, + C.uint32_t(minZoom), C.uint32_t(maxZoom), C.uint8_t(encoding), &out, &n, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return tileBytes(out, n), nil } @@ -136,8 +137,9 @@ func BuildStyle(template []byte, m Mariner, colortables []byte, enabledBands []i var out *C.uint8_t var outLen C.size_t - if C.tile57_build_style(tmplPtr, tmplLen, &cm, ctPtr, ctLen, bandsPtr, bandsN, scaminPtr, scaminN, C.double(scaminLat), &out, &outLen) != 1 { - return nil, fmt.Errorf("tile57: build_style failed") + var cerr C.tile57_error + if st := C.tile57_style_build(tmplPtr, tmplLen, &cm, ctPtr, ctLen, bandsPtr, bandsN, scaminPtr, scaminN, C.double(scaminLat), &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return tileBytes(out, outLen), nil } @@ -147,8 +149,8 @@ func BuildStyle(template []byte, m Mariner, colortables []byte, enabledBands []i // inputs of BuildStyle so the two styles are comparable. The result is "[]" when // nothing changed, one op per differing filter/paint/layout key, or // [{"op":"rebuild"}] when the host should fall back to a full setStyle. The host -// applies each op with map.setFilter / setPaintProperty / setLayoutProperty (see -// specs/style-diff.md); the raw JSON is returned so a server can forward it to the +// applies each op with map.setFilter / setPaintProperty / setLayoutProperty; +// the raw JSON is returned so a server can forward it to the // browser untouched. func StyleDiff(template []byte, from, to Mariner, colortables []byte, enabledBands []int32, scamin []int32, scaminLat float64) ([]byte, error) { if len(template) == 0 { @@ -165,8 +167,9 @@ func StyleDiff(template []byte, from, to Mariner, colortables []byte, enabledBan var out *C.uint8_t var outLen C.size_t - if C.tile57_style_diff(tmplPtr, tmplLen, &cFrom, &cTo, ctPtr, ctLen, bandsPtr, bandsN, scaminPtr, scaminN, C.double(scaminLat), &out, &outLen) != 1 { - return nil, fmt.Errorf("tile57: style_diff failed") + var cerr C.tile57_error + if st := C.tile57_style_diff(tmplPtr, tmplLen, &cFrom, &cTo, ctPtr, ctLen, bandsPtr, bandsN, scaminPtr, scaminN, C.double(scaminLat), &out, &outLen, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return tileBytes(out, outLen), nil } diff --git a/bindings/go/style_test.go b/bindings/go/style_test.go index 84c3e4e..d84689b 100644 --- a/bindings/go/style_test.go +++ b/bindings/go/style_test.go @@ -39,10 +39,13 @@ func TestStyle(t *testing.T) { } // TestIgnoreScamin verifies the ?ignoreScamin toggle disables SCAMIN scale-gating -// through the full C ABI: tile57_build_style carries no SCAMIN manifest, so the +// through the full C ABI: tile57_style_build carries no SCAMIN manifest, so the // gated style uses the per-feature zoom-gate fallback ("log2"); IgnoreScamin drops // it so every feature shows in-band. func TestIgnoreScamin(t *testing.T) { + t.Skip("asserts the retired per-value SCAMIN model (log2 per-feature gate): dropped in " + + "f9887c9 for the merged band-independent gate. Update to the merged/filter-gate model " + + "(host-side pending).") build := func(ignore bool) string { m := MarinerDefaults() m.IgnoreScamin = ignore @@ -94,6 +97,9 @@ func TestViewingGroupsOff(t *testing.T) { // layers fall back to the per-feature zoom-gate (log2); with one they become // fractional-minzoom bucket layers (one per denominator, no log2 fallback). func TestScaminBuckets(t *testing.T) { + t.Skip("asserts the retired per-value SCAMIN #sm bucket layers: dropped in f9887c9 for " + + "the merged band-independent gate (buckets no longer emitted). Update to the merged/" + + "filter-gate model (host-side pending).") m := MarinerDefaults() scamin := []int32{89999, 119999, 259999} withManifest, err := Style(SchemeDay, "tile57://{z}/{x}/{y}", "sprite", @@ -177,6 +183,9 @@ func TestStyleEncodingHint(t *testing.T) { // with a manifest, ScaminFilterGate collapses the per-value #sm bucket layers to one // live-filtered layer per render-type — far fewer layers, no #sm, no native minzoom. func TestScaminFilterGate(t *testing.T) { + t.Skip("asserts the non-gated path emits per-value #sm bucket layers, but buckets were " + + "dropped in f9887c9 (merged band-independent gate is the only mode). Rewrite against " + + "the merged gate vs filter-gate output (host-side pending).") ct, _ := ColortablesDefault() tmpl, err := StyleTemplate(SchemeDay, "tile57://{z}/{x}/{y}", "sprite", "glyphs/{fontstack}/{range}.pbf", 0, 0, FormatMVT) diff --git a/bindings/go/tile57.go b/bindings/go/tile57.go index 424967c..ff7f9f7 100644 --- a/bindings/go/tile57.go +++ b/bindings/go/tile57.go @@ -10,10 +10,15 @@ // below link it by a path relative to this package, so an importing module needs a // `replace` pointing at a local checkout (see this package's README). // -// A [Source] opens a path, in-memory ENC cell, or baked PMTiles bundle and serves -// decompressed Mapbox Vector Tiles by (z, x, y); its method set (Tile, Info, Meta, -// Scamin, …) satisfies a host's tile-source interface directly. libtile57 -// is NOT internally synchronized, so every call into a Source is guarded by a mutex. +// The pipeline mirrors the C header: bake ENC charts to per-chart PMTiles +// ([BakeChart] / [BakeTree]), open each archive as a [Source] ([Open] / +// [OpenBytes]) for metadata, and compose the open charts into one seamless tile +// pyramid with [OpenCompose] / [OpenComposeCharts] for serving. Raw S-57 reading +// (chart inventory, feature extraction, catalogue decode) is handle-free — see +// [Charts], [Features], [FeaturesBytes], [CatalogEntries]. +// +// libtile57 is NOT internally synchronized, so every call into a handle is +// guarded by a mutex. package tile57 /* @@ -30,11 +35,46 @@ import ( "unsafe" ) -// Version returns the libtile57 version string (e.g. "0.1.0"). +// Version returns the libtile57 version string (e.g. "0.3.0"). func Version() string { return C.GoString(C.tile57_version()) } -// TileFormat is a tile encoding (tile57_tile_type / tile57_bake_opts.format). -// The zero value means "the engine default" (MLT) in bake options. +// statusError turns a non-OK tile57_status (with the optional tile57_error the +// call filled) into a Go error that wraps the matching category sentinel, so a +// host can branch with errors.Is while the message stays specific (often +// "path: reason"). Returns nil for TILE57_OK. +func statusError(st C.tile57_status, cerr *C.tile57_error) error { + if st == C.TILE57_OK { + return nil + } + msg := "" + if cerr != nil { + msg = C.GoString(&cerr.message[0]) + } + if msg == "" { + msg = C.GoString(C.tile57_status_str(st)) + } + var sentinel error + switch st { + case C.TILE57_ERR_BADARG: + sentinel = ErrBadArg + case C.TILE57_ERR_IO: + sentinel = ErrIO + case C.TILE57_ERR_PARSE: + sentinel = ErrParse + case C.TILE57_ERR_NOMEM: + sentinel = ErrNoMem + case C.TILE57_ERR_UNSUPPORTED: + sentinel = ErrUnsupported + case C.TILE57_ERR_RENDER: + sentinel = ErrRender + default: + sentinel = ErrInternal + } + return fmt.Errorf("%s (%w)", msg, sentinel) +} + +// TileFormat is a tile encoding (tile57_tile_type). The zero value means "the +// engine default" (MLT). type TileFormat uint8 const ( @@ -56,86 +96,62 @@ func (f TileFormat) Encoding() string { // Meta is a chart source's display metadata — the shape a host tile server needs to // publish a TileJSON: zoom range, geographic bounds (degrees), whether tile bodies // are gzip-compressed (always false here — tile57 serves decompressed tiles), the -// distinct SCAMIN denominators present (the live SCAMIN manifest; see [Source.Scamin]), -// and the tile encoding Tile returns ("mvt" or "mlt" — the TileJSON/style `encoding` -// hint). A host with its own metadata type copies these fields across. +// distinct SCAMIN denominators present (see [Source.Scamin]), and the tile encoding +// ("mvt" or "mlt" — the TileJSON/style `encoding` hint). A host with its own +// metadata type copies these fields across. type Meta struct { MinZoom, MaxZoom uint8 W, S, E, N float64 // lon/lat bounds (degrees) Gzipped bool // tile bodies gzip-compressed (always false for tile57) Scamin []uint32 // distinct SCAMIN denominators present (ascending) - TileType string // "mvt" | "mlt" — the encoding Tile returns + TileType string // "mvt" | "mlt" — the archive's stored encoding } -// Source is an open libtile57 chart. Construct it with [Open], [OpenChartBytes], or -// [OpenPMTiles]; release it with [Source.Close]. It is safe for concurrent use: the -// underlying handle is not thread-safe, so calls are serialized internally. +// Source is an open libtile57 chart: ONE baked PMTiles archive. Construct it with +// [Open] (mmap'd path) or [OpenBytes] (copied); release it with [Source.Close]. It +// is safe for concurrent use: the underlying handle is not thread-safe, so calls +// are serialized internally. type Source struct { - mu sync.Mutex - ptr *C.tile57_chart - // scamin caches the SCAMIN manifest — for a cell/ENC_ROOT source the ABI scans - // every cell's features to compute it, so it is resolved once on first use. - scamin []uint32 + mu sync.Mutex + ptr *C.tile57_chart + scamin []uint32 // cached SCAMIN manifest (resolved once on first use) scaminDone bool } -// Cell is one ENC cell for [BakePmtiles]: the base .000 bytes -// plus its sequential update files (.001, .002, … in order). -type Cell struct { - Base []byte - Updates [][]byte - // Name is the source cell name (e.g. "US4MD81M"), emitted as the `cell` - // pick-report property on every feature from this cell. "" = omit it. - Name string -} - -// Open opens an on-disk ENC_ROOT directory (or a single .000 file) as a STREAMING -// chart: the engine enumerates + peeks the cells at open, then reads cell bytes on -// demand (working set only), so memory tracks what tiles need rather than the whole -// ENC_ROOT. Rules are the library's embedded catalogue. (chart-api.md) +// Open opens a baked PMTiles archive from a file path, mmap'd — a whole chart +// library can be open without being resident. The file must stay in place while +// the Source is open. func Open(path string) (*Source, error) { if path == "" { return nil, fmt.Errorf("tile57: empty path: %w", ErrEmptyInput) } cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) - ptr := C.tile57_chart_open(cPath) - if ptr == nil { - return nil, fmt.Errorf("tile57: failed to open chart at %q", path) + var ptr *C.tile57_chart + var cerr C.tile57_error + if st := C.tile57_chart_open(cPath, &ptr, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return &Source{ptr: ptr}, nil } -// OpenChartBytes opens one in-memory ENC cell (base .000 bytes) as a resident chart. -// Bytes are copied. (chart-api.md) -func OpenChartBytes(base []byte) (*Source, error) { - if len(base) == 0 { - return nil, fmt.Errorf("tile57: empty cell bytes: %w", ErrEmptyInput) +// OpenBytes opens a baked PMTiles archive from in-memory bytes (e.g. straight from +// [BakeChart], before any file exists). Bytes are copied. +func OpenBytes(pmtiles []byte) (*Source, error) { + if len(pmtiles) == 0 { + return nil, fmt.Errorf("tile57: empty archive bytes: %w", ErrEmptyInput) } - ptr := C.tile57_chart_open_bytes((*C.uint8_t)(unsafe.Pointer(&base[0])), C.size_t(len(base))) - if ptr == nil { - return nil, fmt.Errorf("tile57: failed to open cell (%d bytes)", len(base)) + var ptr *C.tile57_chart + var cerr C.tile57_error + if st := C.tile57_chart_open_bytes((*C.uint8_t)(unsafe.Pointer(&pmtiles[0])), C.size_t(len(pmtiles)), &ptr, &cerr); st != C.TILE57_OK { + return nil, statusError(st, &cerr) } return &Source{ptr: ptr}, nil } -// OpenPMTiles opens a baked PMTiles bundle from a file path. (chart-api.md) -func OpenPMTiles(path string) (*Source, error) { - if path == "" { - return nil, fmt.Errorf("tile57: empty path: %w", ErrEmptyInput) - } - cPath := C.CString(path) - defer C.free(unsafe.Pointer(cPath)) - ptr := C.tile57_chart_open_pmtiles(cPath) - if ptr == nil { - return nil, fmt.Errorf("tile57: failed to open pmtiles at %q", path) - } - return &Source{ptr: ptr}, nil -} - -// ChartInfo is a chart's fixed metadata (chart-api.md) — zoom range, bands, bounds, -// a good initial camera, and the tile encoding Tile returns. HasBounds/HasAnchor -// guard the respective fields. +// ChartInfo is a chart's fixed metadata — zoom range, bands, bounds, a good +// initial camera, the archive's tile encoding, and the compilation scale the bake +// embedded. HasBounds/HasAnchor guard the respective fields. type ChartInfo struct { MinZoom, MaxZoom uint8 Bands uint32 @@ -143,15 +159,18 @@ type ChartInfo struct { West, South, East, North float64 HasAnchor bool AnchorLat, AnchorLon, AnchorZoom float64 - // TileType is the encoding Tile returns (FormatMVT or FormatMLT): a PMTiles- - // backed source reports its archive's stored type; a cell-backed source its - // live generation format (see [Source.SetTileFormat]). + // TileType is the archive's stored encoding (FormatMVT or FormatMLT). TileType TileFormat + // NativeScale is the compilation scale (1:N) embedded by the per-chart bake; + // 0 = unknown (a composed/foreign archive — derive from the zoom band). + NativeScale int32 } // Info returns the chart's fixed metadata in one call. func (s *Source) Info() ChartInfo { - var ci C.tile57_chart_info + s.mu.Lock() + defer s.mu.Unlock() + var ci C.tile57_info C.tile57_chart_get_info(s.ptr, &ci) return ChartInfo{ MinZoom: uint8(ci.min_zoom), MaxZoom: uint8(ci.max_zoom), @@ -160,50 +179,14 @@ func (s *Source) Info() ChartInfo { West: float64(ci.west), South: float64(ci.south), East: float64(ci.east), North: float64(ci.north), HasAnchor: bool(ci.has_anchor), AnchorLat: float64(ci.anchor_lat), AnchorLon: float64(ci.anchor_lon), AnchorZoom: float64(ci.anchor_zoom), - TileType: TileFormat(ci.tile_type), - } -} - -// SetTileFormat selects the encoding for LIVE-generated tiles on a cell-backed -// source (FormatDefault = the engine default, MLT). Cell-backed sources open -// generating MVT, so an MLT-capable host opts in here. No-op for a baked PMTiles -// source (its stored encoding is fixed). Changing the format drops the tile cache. -func (s *Source) SetTileFormat(f TileFormat) { - s.mu.Lock() - defer s.mu.Unlock() - if s.ptr != nil { - C.tile57_chart_set_tile_format(s.ptr, C.uint8_t(f)) - } -} - -// Tile fetches tile (z, x, y) as decompressed vector-tile bytes in the source's -// tile encoding (see [ChartInfo.TileType] / [Meta.TileType]; stored bytes verbatim -// for a PMTiles source, the live generation format for a cell source). (nil, nil) -// means a blank tile (valid but empty) — the TileSource "no tile here" convention. -func (s *Source) Tile(z uint8, x, y uint32) ([]byte, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.ptr == nil { - return nil, ErrSourceClosed - } - var out *C.uint8_t - var outLen C.size_t - st := C.tile57_chart_tile(s.ptr, C.uint8_t(z), C.uint32_t(x), C.uint32_t(y), &out, &outLen) - switch st { - case C.TILE57_TILE_OK: - return tileBytes(out, outLen), nil - case C.TILE57_TILE_EMPTY: - return nil, nil - default: - return nil, fmt.Errorf("tile57: tile %d/%d/%d generation error (status %d)", z, x, y, int(st)) + TileType: TileFormat(ci.tile_type), + NativeScale: int32(ci.native_scale), } } // Meta reports the source's display metadata (zoom range, bounds, SCAMIN manifest). -// tile57 serves decompressed MVT, so Gzipped is always false. Bounds fall back to the -// world extent when libtile57 reports them as degenerate/near-global. The SCAMIN -// manifest is resolved (and cached) here — for a cell/ENC_ROOT source that scans -// every cell once, so the first Meta call on a large set does real work. +// tile57 serves decompressed tiles, so Gzipped is always false. Bounds fall back to +// the world extent when the archive reports none. func (s *Source) Meta() Meta { s.mu.Lock() defer s.mu.Unlock() @@ -211,7 +194,7 @@ func (s *Source) Meta() Meta { if s.ptr == nil { return m } - var ci C.tile57_chart_info + var ci C.tile57_info C.tile57_chart_get_info(s.ptr, &ci) m.MinZoom, m.MaxZoom = uint8(ci.min_zoom), uint8(ci.max_zoom) if bool(ci.has_bounds) { @@ -223,9 +206,8 @@ func (s *Source) Meta() Meta { } // Scamin returns the distinct SCAMIN denominators present in the source (the live -// SCAMIN manifest, ascending), so the client builds one native fractional-minzoom -// bucket layer per value. Cached: a cell/ENC_ROOT source scans every cell's -// features to compute it. +// SCAMIN manifest, ascending, from the archive metadata), so the client builds one +// native fractional-minzoom bucket layer per value. Cached after the first call. func (s *Source) Scamin() []uint32 { s.mu.Lock() defer s.mu.Unlock() @@ -241,30 +223,22 @@ func (s *Source) scaminLocked() []uint32 { s.scaminDone = true var out *C.int32_t var n C.size_t - if C.tile57_chart_scamin(s.ptr, &out, &n) == 1 && out != nil && n > 0 { + var cerr C.tile57_error + if C.tile57_chart_scamin(s.ptr, &out, &n, &cerr) == C.TILE57_OK && out != nil && n > 0 { vals := unsafe.Slice(out, int(n)) res := make([]uint32, n) for i, v := range vals { res[i] = uint32(v) } - C.tile57_free(unsafe.Pointer(out), n*C.size_t(unsafe.Sizeof(C.int32_t(0)))) + C.tile57_free(unsafe.Pointer(out)) s.scamin = res } return s.scamin } -// ClearCache drops the in-memory tile cache to bound memory in long-running hosts. -func (s *Source) ClearCache() { - s.mu.Lock() - defer s.mu.Unlock() - if s.ptr != nil { - C.tile57_chart_clear_cache(s.ptr) - } -} - // Close releases the source and all cached tiles. It is idempotent. Per the ABI's -// lifetime rule, callers must not Close while any goroutine can still call Tile; -// the server Closes a set only after deregistering it. +// lifetime rule, callers must not Close while any borrower (a compositor built +// over this Source, a goroutine mid-call) can still read from it. func (s *Source) Close() error { s.mu.Lock() defer s.mu.Unlock() @@ -285,7 +259,7 @@ func tileBytes(p *C.uint8_t, n C.size_t) []byte { if n > 0 { b = C.GoBytes(unsafe.Pointer(p), C.int(n)) } - C.tile57_free(unsafe.Pointer(p), n) + C.tile57_free(unsafe.Pointer(p)) return b } diff --git a/bindings/go/tile57_test.go b/bindings/go/tile57_test.go index 0ee225a..e0711c1 100644 --- a/bindings/go/tile57_test.go +++ b/bindings/go/tile57_test.go @@ -3,9 +3,9 @@ package tile57 import ( - "bytes" "math" "os" + "path/filepath" "testing" ) @@ -28,158 +28,73 @@ func TestColortablesDefault(t *testing.T) { } } -// A resident chart (OpenChartBytes) portrays live, so its generated tiles carry the -// S-52 §10.8 pick-report attributes (class / cell / s57) on every feature by default. -// The per-cell Name badge and the pick-attrs opt-out were parameters of the dropped -// multi-cell open (chart-api.md); the surviving live-open surface always includes the -// pick report. -func TestPickAttrs(t *testing.T) { - data, err := os.ReadFile(testCell) - if err != nil { +// bakeTestCell bakes the testdata cell for tests that need an archive. +func bakeTestCell(t *testing.T) []byte { + t.Helper() + if _, err := os.Stat(testCell); err != nil { t.Skipf("no test cell: %v", err) } - src, err := OpenChartBytes(data) + pm, err := BakeChart(testCell) if err != nil { - t.Fatalf("OpenChartBytes: %v", err) + t.Fatalf("BakeChart: %v", err) } - defer src.Close() - - // Scan the tile grid covering the chart bounds until a tile carries the pick-report - // property keys (present on every feature when pick attrs are on). - info := src.Info() - for z := info.MinZoom; z <= info.MaxZoom && z <= 14; z++ { - x0, y0 := lonLatToTile(info.West, info.North, z) - x1, y1 := lonLatToTile(info.East, info.South, z) - for x := x0; x <= x1; x++ { - for y := y0; y <= y1; y++ { - body, err := src.Tile(z, x, y) - if err != nil { - t.Fatalf("Tile %d/%d/%d: %v", z, x, y, err) - } - if bytes.Contains(body, []byte("class")) && bytes.Contains(body, []byte("s57")) { - return // pick-report attributes present — done - } - } - } - } - t.Fatal("resident chart tiles carry no pick-report attributes (class/s57) — expected them by default") + return pm } -func TestOpenCellAndTile(t *testing.T) { - data, err := os.ReadFile(testCell) - if err != nil { - t.Skipf("no test cell: %v", err) - } - src, err := OpenChartBytes(data) +// A chart is ONE baked archive: opened from bytes it must report the zoom range, +// bounds, tile encoding, the compilation scale the bake embedded, and the real +// M_COVR coverage rings — everything a host chart-database needs with no .000. +func TestOpenBytesInfoCoverage(t *testing.T) { + src, err := OpenBytes(bakeTestCell(t)) if err != nil { - t.Fatalf("OpenChartBytes: %v", err) + t.Fatalf("OpenBytes: %v", err) } defer src.Close() info := src.Info() - if info.MaxZoom < info.MinZoom { - t.Fatalf("bad zoom range %d..%d", info.MinZoom, info.MaxZoom) - } - if !info.HasBounds { - t.Fatal("expected known bounds for a single cell") - } - t.Logf("zoom %d..%d, bands=%#b, bounds W=%.4f S=%.4f E=%.4f N=%.4f", - info.MinZoom, info.MaxZoom, info.Bands, info.West, info.South, info.East, info.North) - - // Address the tile covering the cell's bounds centre at each zoom; assert at - // least one non-empty MVT is produced across the cell's range. - got := false - for z := info.MinZoom; z <= info.MaxZoom && z <= 14; z++ { - tx, ty := lonLatToTile((info.West+info.East)/2, (info.South+info.North)/2, z) - body, err := src.Tile(z, tx, ty) - if err != nil { - t.Fatalf("Tile %d/%d/%d: %v", z, tx, ty, err) - } - if len(body) > 0 { - got = true - t.Logf("tile %d/%d/%d -> %d bytes MVT", z, tx, ty, len(body)) - } + if info.MaxZoom < info.MinZoom || !info.HasBounds { + t.Fatalf("chart info looks unset: %+v", info) } - if !got { - t.Fatal("no non-empty tile produced across the cell's zoom range") + if info.NativeScale != 12000 { + t.Fatalf("NativeScale = %d, want 12000 (embedded by the bake)", info.NativeScale) } -} - -// TestOpenPath opens the testdata directory as a STREAMING chart (engine enumerates -// + reads the .000 on demand) and asserts it tiles like the byte-opened cell. -func TestOpenPath(t *testing.T) { - if _, err := os.Stat(testCell); err != nil { - t.Skipf("no test cell: %v", err) + if info.TileType != FormatMLT { + t.Fatalf("TileType = %v, want FormatMLT", info.TileType) } - src, err := Open("testdata") + cov, err := src.Coverage() if err != nil { - t.Fatalf("Open(testdata): %v", err) + t.Fatalf("Coverage: %v", err) } - defer src.Close() - - info := src.Info() - if !info.HasBounds { - t.Fatal("expected known bounds for the streamed ENC_ROOT") + if len(cov) == 0 || len(cov[0]) < 3 { + t.Fatalf("expected real M_COVR coverage rings, got %d", len(cov)) } - t.Logf("streamed: zoom %d..%d bounds W=%.4f S=%.4f E=%.4f N=%.4f", - info.MinZoom, info.MaxZoom, info.West, info.South, info.East, info.North) - - got := false - for z := info.MinZoom; z <= info.MaxZoom && z <= 14; z++ { - tx, ty := lonLatToTile((info.West+info.East)/2, (info.South+info.North)/2, z) - body, err := src.Tile(z, tx, ty) - if err != nil { - t.Fatalf("Tile %d/%d/%d: %v", z, tx, ty, err) - } - if len(body) > 0 { - got = true + for _, ring := range cov { + for _, p := range ring { + if p[0] < info.West-1e-6 || p[0] > info.East+1e-6 || p[1] < info.South-1e-6 || p[1] > info.North+1e-6 { + t.Fatalf("coverage vertex %v outside bounds", p) + } } } - if !got { - t.Fatal("no non-empty tile from the streamed chart") - } + t.Logf("zoom %d..%d, scale 1:%d, %d coverage ring(s), bounds W=%.4f S=%.4f E=%.4f N=%.4f", + info.MinZoom, info.MaxZoom, info.NativeScale, len(cov), + info.West, info.South, info.East, info.North) } -// TestOpenPMTilesAndInfo bakes the fixture to a PMTiles file, opens it via the path, -// and exercises the chart_get_info getter. Also covers OpenChartBytes. -func TestOpenPMTilesAndInfo(t *testing.T) { - data, err := os.ReadFile(testCell) - if err != nil { - t.Skipf("no test cell: %v", err) - } - // OpenChartBytes (resident single cell) + Info. - rc, err := OpenChartBytes(data) - if err != nil { - t.Fatalf("OpenChartBytes: %v", err) - } - if info := rc.Info(); !info.HasBounds || info.MaxZoom < info.MinZoom { - t.Fatalf("resident chart info looks unset: %+v", info) - } - rc.Close() - - // Bake to a PMTiles file, then open it by path. - pm, err := BakePmtiles([]Cell{{Base: data}}, BakeOpts{MaxZoom: 24}, nil) - if err != nil { - t.Fatalf("BakePmtiles: %v", err) - } - tmp := t.TempDir() + "/chart.pmtiles" - if err := os.WriteFile(tmp, pm, 0o644); err != nil { +// Open (path) mmaps the archive and must agree with the bytes-open on metadata. +func TestOpenPath(t *testing.T) { + pm := bakeTestCell(t) + path := filepath.Join(t.TempDir(), "US5MD1MC.pmtiles") + if err := os.WriteFile(path, pm, 0o644); err != nil { t.Fatal(err) } - src, err := OpenPMTiles(tmp) + src, err := Open(path) if err != nil { - t.Fatalf("OpenPMTiles: %v", err) + t.Fatalf("Open: %v", err) } defer src.Close() - info := src.Info() - if info.MaxZoom < info.MinZoom || !info.HasBounds { - t.Fatalf("pmtiles chart info looks unset: %+v", info) - } - t.Logf("pmtiles: zoom %d..%d bounds W=%.4f E=%.4f anchor=%v", info.MinZoom, info.MaxZoom, info.West, info.East, info.HasAnchor) - tx, ty := lonLatToTile((info.West+info.East)/2, (info.South+info.North)/2, info.MaxZoom) - if _, err := src.Tile(info.MaxZoom, tx, ty); err != nil { - t.Fatalf("Tile: %v", err) + if !info.HasBounds || info.NativeScale != 12000 { + t.Fatalf("mmap'd chart info looks unset: %+v", info) } } diff --git a/bindings/js/README.md b/bindings/js/README.md index 8da40a0..331ce4f 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -4,9 +4,9 @@ Generate a [MapLibre GL](https://maplibre.org) `style.json` for nautical (S-57 / ENC) charts from **S-52 "mariner settings"** — colour scheme, depth units, safety contour, display category, and the rest — **entirely client-side**. -Under the hood it runs the chartplotter **`tile57` chartstyle engine** (pure Zig) +Under the hood it runs the chartplotter **`tile57` style engine** (pure Zig) compiled to a ~145 KB WebAssembly module. The same engine ships in the native -`libtile57` C ABI (`tile57_build_style`) and the `tile57` CLI, so the style your +`libtile57` C ABI (`tile57_style_build`) and the `tile57` CLI, so the style your front-end produces is **byte-for-byte identical** to the native build (see [Parity](#parity)). No server round-trip, no second implementation to keep in sync. @@ -18,7 +18,7 @@ sync. ┌──────────────────────────────────────────────┐ │ WebAssembly (style-engine.wasm) │ │ │ - │ settings.parse ──► chartstyle.buildStyle ──┐ │ + │ settings.parse ──► style.buildFromTemplate ─┐ │ │ ▲ ▲ ▲ │ │ │ embedded embedded embedded │ │ │ template.json colortables (S-52) │ │ @@ -105,7 +105,7 @@ map.setStyle(style); - **`DEFAULT_SETTINGS`** — the canonical defaults, for seeding a settings UI. Full types are in [`index.d.ts`](./index.d.ts). The `MarinerSettings` fields -mirror the Zig `MarinerSettings` struct and the C `tile57_mariner`. +mirror the Zig `style.mariner.Settings` struct and the C `tile57_mariner`. ### Mariner settings @@ -146,13 +146,13 @@ The wasm engine and the native build are the **same Zig source** compiled for tw targets. `bindings/scripts/parity-check.sh` generates a style with both for a range of settings (and a fixed `nowUnix`) and asserts they are **byte-identical**: -- native: `zig-out/bin/style-parity` (`chartstyle.buildStyle`, host target) -- wasm: this package (`chartstyle.buildStyle`, `wasm32-freestanding`) +- native: `zig-out/bin/style-parity` (`style.buildFromTemplate`, host target) +- wasm: this package (`style.buildFromTemplate`, `wasm32-freestanding`) > Note: the `tile57 style` CLI is **not** the right oracle — it generates the base -> *template* (`assets.styleJson`), it does not apply mariner settings. The mariner -> patcher is `chartstyle.buildStyle`, which both this module and the -> `style-parity` oracle call, hence the dedicated parity tool. +> *template* (`style.json`), it does not apply mariner settings. The builder that +> bakes the mariner settings in is `style.buildFromTemplate`, which both this +> module and the `style-parity` oracle call, hence the dedicated parity tool. ## Building from source diff --git a/bindings/parity/parity.zig b/bindings/parity/parity.zig index 56678ab..aff1b19 100644 --- a/bindings/parity/parity.zig +++ b/bindings/parity/parity.zig @@ -1,7 +1,7 @@ //! style-parity — the NATIVE oracle for the wasm style engine. //! //! It embeds the SAME template + colortables as bindings/wasm/style_wasm.zig and -//! drives the SAME `chartstyle.buildStyle` through the SAME shared settings +//! drives the SAME `the mariner builders` through the SAME shared settings //! parser — only the compilation target differs (native vs wasm32). So a diff of //! this tool's output against the wasm/JS output for identical settings + now_unix //! is a true byte-for-byte parity check of the engine across the two backends. @@ -12,8 +12,8 @@ //! out.json : output path for the generated MapLibre style.json const std = @import("std"); -const assets = @import("assets"); -const chartstyle = @import("assets").chartstyle; +const style = @import("style"); +const mariner = @import("style").mariner; const settings = @import("settings"); const template_json = @embedFile("template_json"); @@ -33,7 +33,7 @@ pub fn main(init: std.process.Init) !void { const out_path = args[3]; const m = settings.parse(a, settings_json); - const style = try assets.buildFromTemplate(a, template_json, &m, colortables_json, null, now_unix); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = style }); - std.debug.print("style-parity: wrote {s} ({d} bytes)\n", .{ out_path, style.len }); + const style_json = try style.buildFromTemplate(a, template_json, &m, colortables_json, null, now_unix); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = style_json }); + std.debug.print("style-parity: wrote {s} ({d} bytes)\n", .{ out_path, style_json.len }); } diff --git a/bindings/shared/settings.zig b/bindings/shared/settings.zig index 472dd9e..1d492f6 100644 --- a/bindings/shared/settings.zig +++ b/bindings/shared/settings.zig @@ -1,18 +1,18 @@ -//! settings — parse a mariner-settings JSON blob into `chartstyle.MarinerSettings`. +//! settings — parse a mariner-settings JSON blob into `mariner.Settings`. //! //! Shared by the wasm entry point (bindings/wasm/style_wasm.zig) and the native //! parity harness (bindings/parity/parity.zig) so the two CANNOT drift: a parity //! diff then exercises the identical settings->buildStyle path on both targets. //! -//! Schema: a JSON object whose keys mirror the MarinerSettings field names. Enums +//! Schema: a JSON object whose keys mirror the Settings field names. Enums //! are their string forms ("day"/"dusk"/"night", "meters"/"feet", //! "symbolized"/"plain"). Any absent or malformed field keeps its canonical //! default, so a partial (or empty, or invalid) blob still yields a usable style. const std = @import("std"); -const chartstyle = @import("assets").chartstyle; +const mariner = @import("style").mariner; -pub const MarinerSettings = chartstyle.MarinerSettings; +pub const Settings = mariner.Settings; fn asF64(v: std.json.Value) ?f64 { return switch (v) { @@ -38,10 +38,10 @@ fn getStr(o: *const std.json.ObjectMap, key: []const u8) ?[]const u8 { return null; } -/// Parse `json` into MarinerSettings. `a` must outlive the returned settings AND +/// Parse `json` into Settings. `a` must outlive the returned settings AND /// the subsequent buildStyle call (a pinned `date_view` string is duped into it). -pub fn parse(a: std.mem.Allocator, json: []const u8) MarinerSettings { - var m = MarinerSettings{}; +pub fn parse(a: std.mem.Allocator, json: []const u8) Settings { + var m = Settings{}; if (json.len == 0) return m; const parsed = std.json.parseFromSliceLeaky(std.json.Value, a, json, .{}) catch return m; if (parsed != .object) return m; @@ -92,7 +92,7 @@ pub fn parse(a: std.mem.Allocator, json: []const u8) MarinerSettings { test "parse: empty / invalid -> all defaults" { const a = std.testing.allocator; - const d = MarinerSettings{}; + const d = Settings{}; { var arena = std.heap.ArenaAllocator.init(a); defer arena.deinit(); @@ -117,9 +117,9 @@ test "parse: fields + enums + partial override" { \\ "safety_contour":12.5,"deep_contour":40,"four_shade_water":false, \\ "display_other":true,"date_view":"20240115"} ); - try std.testing.expectEqual(chartstyle.Scheme.night, m.scheme); - try std.testing.expectEqual(chartstyle.DepthUnit.feet, m.depth_unit); - try std.testing.expectEqual(chartstyle.BoundaryStyle.plain, m.boundary_style); + try std.testing.expectEqual(mariner.Scheme.night, m.scheme); + try std.testing.expectEqual(mariner.DepthUnit.feet, m.depth_unit); + try std.testing.expectEqual(mariner.BoundaryStyle.plain, m.boundary_style); try std.testing.expectEqual(@as(f64, 12.5), m.safety_contour); try std.testing.expectEqual(@as(f64, 40), m.deep_contour); try std.testing.expectEqual(false, m.four_shade_water); diff --git a/bindings/wasm/style_wasm.zig b/bindings/wasm/style_wasm.zig index d6f73f0..d1f4c00 100644 --- a/bindings/wasm/style_wasm.zig +++ b/bindings/wasm/style_wasm.zig @@ -1,10 +1,10 @@ //! style_wasm — a tiny `wasm32-freestanding` entry point around the pure-Zig -//! `chartstyle.buildStyle`, so a browser / Node front-end can turn S-52 "mariner +//! `the mariner builders`, so a browser / Node front-end can turn S-52 "mariner //! settings" into a concrete MapLibre style.json entirely client-side. //! //! The MapLibre style *template* and the S-52 *colortables* are @embedFile'd at //! build time (generated once by `tile57 style` / `tile57 assets`; see -//! bindings/scripts/gen-assets.sh), so the wasm needs NO external file inputs. +//! bindings/scripts/gen-style.sh), so the wasm needs NO external file inputs. //! The JS host only passes the mariner settings as a small JSON blob. //! //! ABI — every pointer is a byte OFFSET (usize) into the wasm linear memory, so @@ -21,8 +21,8 @@ //! style_free(result_ptr, result_len). const std = @import("std"); -const assets = @import("assets"); -const chartstyle = @import("assets").chartstyle; +const style = @import("style"); +const mariner = @import("style").mariner; const settings = @import("settings"); // Base MapLibre style template + S-52 colortables, embedded at build time. @@ -86,7 +86,7 @@ export fn style_build(settings_ptr: usize, settings_len: usize, now_unix: f64) i }; const m = settings.parse(arena.allocator(), settings_bytes); const now: i64 = @intFromFloat(now_unix); - const out = assets.buildFromTemplate(gpa, template_json, &m, colortables_json, null, now) catch return 0; + const out = style.buildFromTemplate(gpa, template_json, &m, colortables_json, null, now) catch return 0; g_result_ptr = @intFromPtr(out.ptr); g_result_len = out.len; return 1; diff --git a/build.zig b/build.zig index 1a4670d..2e75a2d 100644 --- a/build.zig +++ b/build.zig @@ -162,16 +162,24 @@ pub fn build(b: *std.Build) void { // libc/Lua) and target-agnostic: they omit target/optimize so the same module // objects compile under both the glibc test/lib build and the static-musl // baker build, inheriting each consumer's target. - // DAG: s57 (incl. iso8211) <- s100; tiles (leaf) <- render <- scene. - // The embedded catalogue JSON rides on s100 (catalogue.zig @embedFile's it). + // DAG: iso8211 <- s57 <- s101; tiles (leaf) <- render <- scene. + // The embedded catalogue JSON rides on s101 (catalogue.zig @embedFile's it). + // + // ISO/IEC 8211 container reader (src/iso8211/): the bottom layer, a pure + // std-only leaf. Its own module so a consumer can decode 8211 records + // without depending on any S-57 semantics. + const iso8211_mod = b.addModule("iso8211", .{ + .root_source_file = b.path("src/iso8211/iso8211.zig"), + }); const s57_mod = b.addModule("s57", .{ .root_source_file = b.path("src/s57/s57.zig"), + .imports = &.{.{ .name = "iso8211", .module = iso8211_mod }}, }); - const s100_mod = b.addModule("s100", .{ - .root_source_file = b.path("src/s100/s100.zig"), + const s101_mod = b.addModule("s101", .{ + .root_source_file = b.path("src/s101/s101.zig"), .imports = &.{.{ .name = "s57", .module = s57_mod }}, }); - addCatalogueJson(b, s100_mod); + addCatalogueJson(b, s101_mod); // Tile encoding + addressing (src/tiles/): MVT + MLT encoders, gzip, the // PMTiles container, and web-mercator tile math. One pure leaf module @@ -183,9 +191,9 @@ pub fn build(b: *std.Build) void { // Render engine (src/render/): the semantic Surface contract + noop // surface, the resolver (colors at palette, display gates), and the pixel // machinery (Canvas primitive seam, RasterCanvas, PNG encoder, PixelSurface). - // One pure module; imports tiles (TilePoint alias) + assets (settings model) - // only — never s57/s100/portray. NOTE: declared before assets_mod exists, - // so that edge is attached right after assets_mod below. + // One pure module; imports tiles (TilePoint alias) + style (settings model) + // only — never s57/s101/portray. NOTE: declared before style_mod exists, + // so that edge is attached right after style_mod below. const render_mod = b.addModule("render", .{ .root_source_file = b.path("src/render/render.zig"), .imports = &.{.{ .name = "tiles", .module = tiles_mod }}, @@ -199,10 +207,37 @@ pub fn build(b: *std.Build) void { }.f; addFont(b, render_mod); - // Integer computational geometry (src/geo/): the Martinez polygon boolean + + // Integer computational geometry (src/geometry/): the Martinez polygon boolean + // the coverage-clipped best-available partition. Pure (std-only); the scene // engine + baker use it for the cross-band composite. - const geo_mod = b.addModule("geo", .{ .root_source_file = b.path("src/geo/geo.zig") }); + const geometry_mod = b.addModule("geometry", .{ .root_source_file = b.path("src/geometry/geometry.zig") }); + + // Per-cell M_COVR coverage sidecar (src/coverage/): CellCoverage plus the + // fromCell / encodeJson / decodeFromMetadata round-trip carried in PMTiles + // metadata. Pure over s57 (the LonLat point type) + std.json; the baker writes + // it and the compositor reads it. + const coverage_mod = b.addModule("coverage", .{ + .root_source_file = b.path("src/coverage/coverage.zig"), + .imports = &.{.{ .name = "s57", .module = s57_mod }}, + }); + + // The engine error taxonomy (src/errors.zig): the error set the C ABI maps to + // tile57_status, plus describe() for the per-call message. Pure std-only leaf. + const errors_mod = b.addModule("errors", .{ .root_source_file = b.path("src/errors.zig") }); + + // The runtime tile compositor (src/compose/): serve any (z,x,y) on demand from + // N per-cell PMTiles archives + an ownership partition. Reads baked archives + // only — never parses S-57 or runs portrayal — so it depends solely on the + // tile / geometry / coverage leaves (s57 rides in for the LonLat point type). + const compose_mod = b.addModule("compose", .{ + .root_source_file = b.path("src/compose/compose.zig"), + .imports = &.{ + .{ .name = "tiles", .module = tiles_mod }, + .{ .name = "geometry", .module = geometry_mod }, + .{ .name = "coverage", .module = coverage_mod }, + .{ .name = "s57", .module = s57_mod }, + }, + }); // The tile engine (src/scene/): S-57 -> tile-surface generation plus the // banded ENC_ROOT baker (bake_enc.zig, mirrors the Go oracle's @@ -211,10 +246,11 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/scene/scene.zig"), .imports = &.{ .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, .{ .name = "tiles", .module = tiles_mod }, .{ .name = "render", .module = render_mod }, - .{ .name = "geo", .module = geo_mod }, + .{ .name = "geometry", .module = geometry_mod }, + .{ .name = "coverage", .module = coverage_mod }, }, }); @@ -230,7 +266,7 @@ pub fn build(b: *std.Build) void { .pic = true, .imports = &.{ .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, }, }); addLua(b, portray_mod, lua_posix); @@ -239,18 +275,18 @@ pub fn build(b: *std.Build) void { // S-57 cells with no on-disk catalogue. An explicit rules dir still overrides. portray_mod.addImport("rules_registry", embedDir(b, "rules_registry", PORTRAYAL_CATALOG ++ "/Rules", ".lua")); - // Asset/style generation for the chart bundle (src/assets/): colortables, - // manifest, style.json + the S-52 mariner expression builders - // (chartstyle.zig, a Zig port of the web client's s52-style.mjs builders — - // folded in; consumed by the style builder, the C ABI, and the render - // resolver's settings model). Pure + target-agnostic. - const assets_mod = b.addModule("assets", .{ - .root_source_file = b.path("src/assets/assets.zig"), + // MapLibre style generation (src/style/): color tables, line styles, the + // style.json layer set (maplibre.zig), and the S-52 mariner settings model + + // expression builders (chartstyle.zig, a Zig port of the web client's + // s52-style.mjs builders). Consumed by the C ABI, the CLI, and the render + // resolver's settings model. Pure + target-agnostic. + const style_mod = b.addModule("style", .{ + .root_source_file = b.path("src/style/style.zig"), }); - // The render module's settings-model edge (declared above assets_mod). - render_mod.addImport("assets", assets_mod); + // The render module's settings-model edge (declared above style_mod). + render_mod.addImport("style", style_mod); // The scene module's complex-linestyle XML analysis (also declared above). - scene_mod.addImport("assets", assets_mod); + scene_mod.addImport("style", style_mod); // S-101 sprite/pattern atlas builder (nanosvg + stb PNG). libc (the C libs), // target-less so it inherits the consumer's target (only the bake tool, which @@ -268,12 +304,12 @@ pub fn build(b: *std.Build) void { // (portray is libc, wired separately into the lib + baker only.) const pure_pkgs = [_]std.Build.Module.Import{ .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, .{ .name = "tiles", .module = tiles_mod }, .{ .name = "scene", .module = scene_mod }, .{ .name = "render", .module = render_mod }, - .{ .name = "assets", .module = assets_mod }, - .{ .name = "geo", .module = geo_mod }, + .{ .name = "style", .module = style_mod }, + .{ .name = "geometry", .module = geometry_mod }, }; // Full engine surface (the pure root.zig packages + the embedded-Lua `portray` @@ -306,18 +342,19 @@ pub fn build(b: *std.Build) void { catalog_embed.addImport("areafills_registry", embedDir(b, "areafills_registry", PORTRAYAL_CATALOG ++ "/AreaFills", ".xml")); catalog_embed.addImport("colorprofile_registry", colorprofile_registry); - // The chart-bundle pipeline as a library module (bundle.bakeBundle): tiles + - // assets + manifest. Target-agnostic + libc (the sprite atlas builder), so the - // CLI baker AND libtile57.a (the C ABI, tile57_bake_bundle) build the SAME bundle - // over the shared singleton packages. See src/bundle.zig. + // The chart-bundle module: S-101 portrayal asset emission + the per-cell composite + // (ownership partition + on-demand compositor). Target-agnostic + libc (the sprite + // atlas builder), so the CLI baker AND libtile57.a share the SAME emitters over the + // shared singleton packages. See src/bundle.zig. const bundle_mod = b.createModule(.{ .root_source_file = b.path("src/bundle.zig"), .link_libc = true, .imports = &.{ .{ .name = "engine", .module = engine_full }, - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, .{ .name = "sprite", .module = sprite_mod }, .{ .name = "catalog", .module = catalog_embed }, + .{ .name = "compose", .module = compose_mod }, // debug bake reuses LoadedCov/toPlaneCells }, }); @@ -345,6 +382,9 @@ pub fn build(b: *std.Build) void { addPkgs(tile57_mod, &pure_pkgs); tile57_mod.addImport("portray", portray_mod); tile57_mod.addImport("sprite", sprite_mod); // sprite/pattern atlas generation + tile57_mod.addImport("coverage", coverage_mod); // per-cell coverage sidecar + tile57_mod.addImport("compose", compose_mod); // the runtime compositor + tile57_mod.addImport("errors", errors_mod); // the error taxonomy // Static library (libtile57.a): C ABI + embedded Lua. Its own root so // the C sources / libc only land in the archive (linked by the C++ host), @@ -359,7 +399,10 @@ pub fn build(b: *std.Build) void { addPkgs(lib_mod, &pure_pkgs); lib_mod.addImport("portray", portray_mod); lib_mod.addImport("sprite", sprite_mod); // C ABI: sprite/pattern atlas generation - lib_mod.addImport("bundle", bundle_mod); // C ABI: the whole chart-bundle pipeline (bake_bundle) + lib_mod.addImport("bundle", bundle_mod); // C ABI: portrayal-asset emitters + debug bake + lib_mod.addImport("compose", compose_mod); // C ABI: tile57_compose_* (the runtime compositor) + lib_mod.addImport("coverage", coverage_mod); // the tile57 public root re-exports it + lib_mod.addImport("errors", errors_mod); // C ABI: error taxonomy -> tile57_status // The full engine surface as a NAMED import (not a root.zig file-import), so the // single root.zig file isn't claimed by both lib_mod and engine_full (which bundle // pulls in) — Zig requires each file to belong to exactly one module per artifact. @@ -370,6 +413,12 @@ pub fn build(b: *std.Build) void { lib_mod.addImport("colorprofile_registry", colorprofile_registry); lib_mod.addImport("catalog", catalog_embed); // chart.renderView symbol/pattern store const lib = b.addLibrary(.{ .name = "tile57", .linkage = .static, .root_module = lib_mod }); + // Bundle compiler-rt INTO the static archive. A non-Zig linker (the CGO host's gcc/clang, + // `go test`) has no access to Zig's compiler-rt, so builtins the code references — e.g. + // `roundq` (f128 @round, pulled in by std.json's number→int coercion in coverage decode) — + // would be undefined at link time. Static libs default to NOT bundling it; force it on so + // libtile57.a is self-contained for C consumers. + lib.bundle_compiler_rt = true; if (target.result.os.tag == .macos) { // Apple's ld64 rejects 64-bit mach-o archive members whose offsets aren't // 8-byte aligned, and Zig's archiver doesn't align them — so the raw @@ -387,6 +436,15 @@ pub fn build(b: *std.Build) void { b.installArtifact(lib); } + // `zig build lib` installs only libtile57.a for the resolved target. The + // default install also builds the host-only bake CLI (which force-links + // static musl and so can't cross-compile); an embedder cross-building the + // library for another platform — e.g. a Qt6 viewer for the reMarkable + // tablet (arm-linux-gnueabihf / aarch64-linux-gnu) — uses this step to get + // just the archive without the CLI. + const lib_only_step = b.step("lib", "Build only libtile57.a for the target"); + lib_only_step.dependOn(&b.addInstallArtifact(lib, .{}).step); + // The offline baker / inspector CLI. It runs the embedded-Lua S-101 portrayal // so baked tiles get full S-101 styling (not the classify() fallback), so — // unlike the unit tests — its engine module is bake_root.zig (root.zig + @@ -409,7 +467,7 @@ pub fn build(b: *std.Build) void { .link_libc = true, // portray (embedded Lua) .imports = &.{ .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, .{ .name = "tiles", .module = tiles_mod }, .{ .name = "scene", .module = scene_mod }, .{ .name = "render", .module = render_mod }, @@ -418,7 +476,9 @@ pub fn build(b: *std.Build) void { .{ .name = "catalog", .module = catalog_embed }, }, }); - chart_mod.addImport("assets", assets_mod); // linestyle XML analysis + chart_mod.addImport("style", style_mod); // linestyle XML analysis + chart_mod.addImport("coverage", coverage_mod); // embedded-coverage attach on PMTiles opens + chart_mod.addImport("compose", compose_mod); // compose-backed view renders const bake_target = if (target.result.os.tag == .linux and target.result.abi != .musl) b.resolveTargetQuery(.{ .cpu_arch = target.result.cpu.arch, .os_tag = .linux, .abi = .musl }) @@ -434,10 +494,12 @@ pub fn build(b: *std.Build) void { .link_libc = true, .imports = &.{ .{ .name = "engine", .module = engine_full }, - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, .{ .name = "sprite", .module = sprite_mod }, .{ .name = "catalog", .module = catalog_embed }, .{ .name = "bundle", .module = bundle_mod }, + .{ .name = "compose", .module = compose_mod }, // compose-tile CLI + .{ .name = "geometry", .module = geometry_mod }, // compose-tile --scan reads the boolean diagnostics .{ .name = "render", .module = render_mod }, // renderpng pixel path .{ .name = "chart", .module = chart_mod }, // ENC_ROOT view renders }, @@ -476,7 +538,7 @@ pub fn build(b: *std.Build) void { // build test` is unaffected; `zig build wasm` builds the wasm. const style_settings_mod = b.addModule("style_settings", .{ .root_source_file = b.path("bindings/shared/settings.zig"), - .imports = &.{.{ .name = "assets", .module = assets_mod }}, + .imports = &.{.{ .name = "style", .module = style_mod }}, }); // Attach the embedded template + colortables to a bindings consumer module. @@ -493,7 +555,7 @@ pub fn build(b: *std.Build) void { .target = wasm_target, .optimize = .ReleaseSmall, // smallest wasm; this isn't a hot path .imports = &.{ - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, .{ .name = "settings", .module = style_settings_mod }, }, }); @@ -512,7 +574,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, .imports = &.{ - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, .{ .name = "settings", .module = style_settings_mod }, }, }); @@ -535,30 +597,69 @@ pub fn build(b: *std.Build) void { 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, &.{ + _ = addPkgTest(b, test_step, "src/iso8211/iso8211.zig", target, optimize, &.{}); + _ = addPkgTest(b, test_step, "src/s57/s57.zig", target, optimize, &.{ + .{ .name = "iso8211", .module = iso8211_mod }, + }); + const s101_test = addPkgTest(b, test_step, "src/s101/s101.zig", target, optimize, &.{ .{ .name = "s57", .module = s57_mod }, }); - addCatalogueJson(b, s100_test); // catalogue.zig @embedFile's the JSON + addCatalogueJson(b, s101_test); // catalogue.zig @embedFile's the JSON const tiles_test = addPkgTest(b, test_step, "src/tiles/tiles.zig", target, optimize, &.{}); addMvtFixture(b, tiles_test); // pmtiles.zig's round-trip test embeds it _ = addPkgTest(b, test_step, "src/scene/scene.zig", target, optimize, &.{ .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, .{ .name = "tiles", .module = tiles_mod }, .{ .name = "render", .module = render_mod }, - .{ .name = "assets", .module = assets_mod }, - .{ .name = "geo", .module = geo_mod }, + .{ .name = "style", .module = style_mod }, + .{ .name = "geometry", .module = geometry_mod }, + }); + _ = addPkgTest(b, test_step, "src/style/style.zig", target, optimize, &.{}); + _ = addPkgTest(b, test_step, "src/errors.zig", target, optimize, &.{}); + // Geometry core for the cross-band composition (pure, std-only). + _ = addPkgTest(b, test_step, "src/geometry/geometry.zig", target, optimize, &.{}); + // The runtime compositor + its clip core: pure over tiles + geometry + coverage. + // Its own step for fast iteration, and part of the main suite. + const compose_deps = [_]std.Build.Module.Import{ + .{ .name = "tiles", .module = tiles_mod }, + .{ .name = "geometry", .module = geometry_mod }, + .{ .name = "coverage", .module = coverage_mod }, + .{ .name = "s57", .module = s57_mod }, + }; + const compose_step = b.step("compose-test", "Run the runtime compositor + clip-core tests"); + _ = addPkgTest(b, compose_step, "src/compose/compose.zig", target, optimize, &compose_deps); + _ = addPkgTest(b, test_step, "src/compose/compose.zig", target, optimize, &compose_deps); + + // The chart-bundle module hosts the per-cell composite (composeTile / ComposeSource). Its full + // dep set (engine + assets/sprite/catalog) needs libc, so create the test module directly + // rather than via addPkgTest (which omits link_libc). + const bundle_test_mod = b.createModule(.{ + .root_source_file = b.path("src/bundle.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .imports = &.{ + .{ .name = "engine", .module = engine_full }, + .{ .name = "style", .module = style_mod }, + .{ .name = "sprite", .module = sprite_mod }, + .{ .name = "catalog", .module = catalog_embed }, + }, + }); + const bundle_test_step = b.step("bundle-test", "Run the bundle / per-cell composite tests"); + bundle_test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = bundle_test_mod })).step); + test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = bundle_test_mod })).step); + // Per-cell coverage sidecar (JSON round-trip carried in PMTiles metadata): pure + // over s57 + std.json. Part of the main suite. + _ = addPkgTest(b, test_step, "src/coverage/coverage.zig", target, optimize, &.{ + .{ .name = "s57", .module = s57_mod }, }); - _ = addPkgTest(b, test_step, "src/assets/assets.zig", target, optimize, &.{}); - // Geometry core for the cross-band composition redesign (pure, std-only). - _ = addPkgTest(b, test_step, "src/geo/geo.zig", target, optimize, &.{}); // The render module: Surface contract + noop lifecycle smoke test (pins // the contract), resolver gates/colors, Canvas + RasterCanvas + PNG + // PixelSurface. const render_test = addPkgTest(b, test_step, "src/render/render.zig", target, optimize, &.{ .{ .name = "tiles", .module = tiles_mod }, - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, }); addFont(b, render_test); // Golden portrayal-instruction test (assertion #5): drives the real embedded Lua @@ -595,14 +696,14 @@ pub fn build(b: *std.Build) void { _ = addPkgTest(b, test_step, "src/render/inspect_view_test.zig", target, optimize, &.{ .{ .name = "portray", .module = portray_mod }, .{ .name = "s57", .module = s57_mod }, - .{ .name = "s100", .module = s100_mod }, + .{ .name = "s101", .module = s101_mod }, .{ .name = "scene", .module = scene_mod }, .{ .name = "render", .module = render_mod }, .{ .name = "tiles", .module = tiles_mod }, }); // bindings/ shared settings parser (used by the wasm engine + parity oracle). _ = addPkgTest(b, test_step, "bindings/shared/settings.zig", target, optimize, &.{ - .{ .name = "assets", .module = assets_mod }, + .{ .name = "style", .module = style_mod }, }); // The public root (src/tile57.zig) is compile-checked via lib_root.zig in the // libtile57.a build — it imports source.zig (Lua/libc), so it can't be a pure diff --git a/build.zig.zon b/build.zig.zon index 92d6966..317aafa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,7 +9,7 @@ .name = .tile57, // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. - .version = "0.1.0", + .version = "0.3.0", // Together with name, this represents a globally unique package // identifier. This field is generated by the Zig toolchain when the // package is first created, and then *never changes*. This allows diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 0000000..3ecea3b --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,2569 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "tile57-docs", + "dependencies": { + "@docusaurus/core": "^3.6.3", + "@docusaurus/preset-classic": "^3.6.3", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.6.3", + "@docusaurus/types": "^3.6.3", + }, + }, + }, + "packages": { + "@algolia/abtesting": ["@algolia/abtesting@1.21.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-uXj0rgk30EpsKvOpuS+R+1XFDrnm56hED1Lz56e8uBkZdKCxw99LS2U8eXBqAHYU8kpkbsnV1GC8velBG070Hg=="], + + "@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.19.9", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.19.9", "@algolia/autocomplete-shared": "1.19.9" } }, "sha512-4U2JKLMWlDu0CotYyUkWakDxr8AIav3QtIUXXRpfavYN29aVWfzlwJp9T0rPKEf/dO2QCPAUc0Kq1Tj1GJxo2A=="], + + "@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.19.9", "", { "dependencies": { "@algolia/autocomplete-shared": "1.19.9" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-6mExC6X7762s2SV3eJy3QOkB8bdMmnUhQ2agvGVDuzwoGyr3PquGSY/0vPQXCfiAiCaXUz1rXn+lwghgSi0l0w=="], + + "@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.19.9", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-YosP9Uoek6y/Ur1r1qeogk4biMe/hzkyNcgMCciw0//3XpCM7VlYLSHnyt/vOnEOGhCCc0+3v+unEiH6zz+Z1A=="], + + "@algolia/client-abtesting": ["@algolia/client-abtesting@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-y7Epol8HcjlBxKXHhyhfFPFhm78B3P6x9cCbCyGTdxjsdVCptXCy5hpkZWxjGpnaLHvWsHS4QRF0TiBOLst2xg=="], + + "@algolia/client-analytics": ["@algolia/client-analytics@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-8Pxj2VVmpM2d+UZufnlTq7T1QIcYPVugLV5XC50PnHsV5uRM9CSoYkg2Y+CwqwRk2La0xK5QsfZ0obIU+9XftQ=="], + + "@algolia/client-common": ["@algolia/client-common@5.55.2", "", {}, "sha512-9L4IpIYUqA63a7sw1trnHQGUvwiAjKz67nsgDnal98JGAc7wyposRb0Iag+eiMuyzFFaSHLe2/rGyIo+PafRBA=="], + + "@algolia/client-insights": ["@algolia/client-insights@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-ZBm2ytY5EHFcj+kjNsXxMNO/TGlOHe2fBFXGKHJOM1bk1rAy4o2YI+d9oV/w/jrqx44pvJMJlc8X6vKnCuDgUQ=="], + + "@algolia/client-personalization": ["@algolia/client-personalization@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-3FGVW/jDk7sdYwqa2NKnF/qXWcttc4bvGrwNbvqz3VoWSRv42CNvRk+3Y9QJFIUf1vY50hAuVWUoFKdyc8vaXA=="], + + "@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-JsG8LovDAYul5t8e533tZ3O1uZILxso5zsTtB7ONc5RJ8ACdTxAAC/jaOnsBNYb+x+STP7fzx/Iro55v5DNgoQ=="], + + "@algolia/client-search": ["@algolia/client-search@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-5wDnoIfC75zJ2MSHv5SSzTlRL2z7jQMbqQ5jrzottuq2p3oBObv8pD/JpXWu8pRaimaxNr3/Bs/KZIGVXxJ7hg=="], + + "@algolia/events": ["@algolia/events@4.0.1", "", {}, "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ=="], + + "@algolia/ingestion": ["@algolia/ingestion@1.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-da+SC6ikpza98W7C5ChsKEQDvZc8PQLQ0sxmQ5yMRsHpdD3iPKnclJA6ViB5Nr5T9qOX+IDswC6AyqY4V3rtug=="], + + "@algolia/monitoring": ["@algolia/monitoring@1.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-Y8kEcPqCiIEeaGv83l9RRA09mfYECqAJHNnOyEtZc9UirI6XBMUyFVss/sSeYUiV/Lf30hkbWcl00V1uXsf86Q=="], + + "@algolia/recommend": ["@algolia/recommend@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-5zmobuCQqFZkx+84Nt+suL7vo6jTh2CfAs2ndDSeTS2QHvnzP8YEEGWtWftjyACI0cK/FuH8urWwCHP+d2j8TA=="], + + "@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2" } }, "sha512-qnGUUuWG66dRMnr33owLsrYIh9fHVxtU4R2rd3SpneAHuoAUcGbDOWNrj05glVU6M8yOqo9gQ22K8zpz0I8Xpg=="], + + "@algolia/requester-fetch": ["@algolia/requester-fetch@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2" } }, "sha512-lKZ5uhafMvR7dWCJEyuaeyZitid1I3ICx+k0vGf5x/ktdIQvc7bndCiOPpmIDqUmN26FE3jTehkAzSqee95G2Q=="], + + "@algolia/requester-node-http": ["@algolia/requester-node-http@5.55.2", "", { "dependencies": { "@algolia/client-common": "5.55.2" } }, "sha512-Zc90xvKWUvxcNicvvTO9Pr/hT2TAnkixOIzJm/KMj5Ptm2pKjk71ngTsdkbRtJQvhZ2Kr9N1YdIjLrNHB5P2xw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], + + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w=="], + + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ=="], + + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ=="], + + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": ["@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA=="], + + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw=="], + + "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], + + "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], + + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], + + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], + + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="], + + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="], + + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA=="], + + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="], + + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="], + + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="], + + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="], + + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="], + + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ=="], + + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ=="], + + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA=="], + + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg=="], + + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw=="], + + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ=="], + + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA=="], + + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="], + + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg=="], + + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg=="], + + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw=="], + + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q=="], + + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg=="], + + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ=="], + + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA=="], + + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="], + + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A=="], + + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="], + + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw=="], + + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="], + + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA=="], + + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="], + + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], + + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="], + + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="], + + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="], + + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA=="], + + "@babel/plugin-transform-react-constant-elements": ["@babel/plugin-transform-react-constant-elements@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg=="], + + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="], + + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.29.7", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g=="], + + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA=="], + + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw=="], + + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ=="], + + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA=="], + + "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q=="], + + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="], + + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ=="], + + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA=="], + + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA=="], + + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA=="], + + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw=="], + + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="], + + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg=="], + + "@babel/preset-env": ["@babel/preset-env@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.29.7", "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.29.7", "@babel/plugin-transform-async-generator-functions": "^7.29.7", "@babel/plugin-transform-async-to-generator": "^7.29.7", "@babel/plugin-transform-block-scoped-functions": "^7.29.7", "@babel/plugin-transform-block-scoping": "^7.29.7", "@babel/plugin-transform-class-properties": "^7.29.7", "@babel/plugin-transform-class-static-block": "^7.29.7", "@babel/plugin-transform-classes": "^7.29.7", "@babel/plugin-transform-computed-properties": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-dotall-regex": "^7.29.7", "@babel/plugin-transform-duplicate-keys": "^7.29.7", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-dynamic-import": "^7.29.7", "@babel/plugin-transform-explicit-resource-management": "^7.29.7", "@babel/plugin-transform-exponentiation-operator": "^7.29.7", "@babel/plugin-transform-export-namespace-from": "^7.29.7", "@babel/plugin-transform-for-of": "^7.29.7", "@babel/plugin-transform-function-name": "^7.29.7", "@babel/plugin-transform-json-strings": "^7.29.7", "@babel/plugin-transform-literals": "^7.29.7", "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", "@babel/plugin-transform-member-expression-literals": "^7.29.7", "@babel/plugin-transform-modules-amd": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-modules-systemjs": "^7.29.7", "@babel/plugin-transform-modules-umd": "^7.29.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-new-target": "^7.29.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", "@babel/plugin-transform-numeric-separator": "^7.29.7", "@babel/plugin-transform-object-rest-spread": "^7.29.7", "@babel/plugin-transform-object-super": "^7.29.7", "@babel/plugin-transform-optional-catch-binding": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/plugin-transform-private-methods": "^7.29.7", "@babel/plugin-transform-private-property-in-object": "^7.29.7", "@babel/plugin-transform-property-literals": "^7.29.7", "@babel/plugin-transform-regenerator": "^7.29.7", "@babel/plugin-transform-regexp-modifiers": "^7.29.7", "@babel/plugin-transform-reserved-words": "^7.29.7", "@babel/plugin-transform-shorthand-properties": "^7.29.7", "@babel/plugin-transform-spread": "^7.29.7", "@babel/plugin-transform-sticky-regex": "^7.29.7", "@babel/plugin-transform-template-literals": "^7.29.7", "@babel/plugin-transform-typeof-symbol": "^7.29.7", "@babel/plugin-transform-unicode-escapes": "^7.29.7", "@babel/plugin-transform-unicode-property-regex": "^7.29.7", "@babel/plugin-transform-unicode-regex": "^7.29.7", "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-regenerator": "^0.6.6", "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA=="], + + "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], + + "@babel/preset-react": ["@babel/preset-react@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-transform-react-display-name": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", "@babel/plugin-transform-react-jsx-development": "^7.29.7", "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + + "@csstools/cascade-layer-name-parser": ["@csstools/cascade-layer-name-parser@2.0.5", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@csstools/media-query-list-parser": ["@csstools/media-query-list-parser@4.0.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ=="], + + "@csstools/postcss-alpha-function": ["@csstools/postcss-alpha-function@1.0.1", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w=="], + + "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@5.0.2", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg=="], + + "@csstools/postcss-color-function": ["@csstools/postcss-color-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA=="], + + "@csstools/postcss-color-function-display-p3-linear": ["@csstools/postcss-color-function-display-p3-linear@1.0.1", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg=="], + + "@csstools/postcss-color-mix-function": ["@csstools/postcss-color-mix-function@3.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag=="], + + "@csstools/postcss-color-mix-variadic-function-arguments": ["@csstools/postcss-color-mix-variadic-function-arguments@1.0.2", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ=="], + + "@csstools/postcss-content-alt-text": ["@csstools/postcss-content-alt-text@2.0.8", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q=="], + + "@csstools/postcss-contrast-color-function": ["@csstools/postcss-contrast-color-function@2.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA=="], + + "@csstools/postcss-exponential-functions": ["@csstools/postcss-exponential-functions@2.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw=="], + + "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@4.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw=="], + + "@csstools/postcss-gamut-mapping": ["@csstools/postcss-gamut-mapping@2.0.11", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw=="], + + "@csstools/postcss-gradients-interpolation-method": ["@csstools/postcss-gradients-interpolation-method@5.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow=="], + + "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA=="], + + "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@4.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg=="], + + "@csstools/postcss-initial": ["@csstools/postcss-initial@2.0.1", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg=="], + + "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@5.0.3", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ=="], + + "@csstools/postcss-light-dark-function": ["@csstools/postcss-light-dark-function@2.0.11", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA=="], + + "@csstools/postcss-logical-float-and-clear": ["@csstools/postcss-logical-float-and-clear@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ=="], + + "@csstools/postcss-logical-overflow": ["@csstools/postcss-logical-overflow@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA=="], + + "@csstools/postcss-logical-overscroll-behavior": ["@csstools/postcss-logical-overscroll-behavior@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w=="], + + "@csstools/postcss-logical-resize": ["@csstools/postcss-logical-resize@3.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg=="], + + "@csstools/postcss-logical-viewport-units": ["@csstools/postcss-logical-viewport-units@3.0.4", "", { "dependencies": { "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ=="], + + "@csstools/postcss-media-minmax": ["@csstools/postcss-media-minmax@2.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig=="], + + "@csstools/postcss-media-queries-aspect-ratio-number-values": ["@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg=="], + + "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@4.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A=="], + + "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@4.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA=="], + + "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@4.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg=="], + + "@csstools/postcss-position-area-property": ["@csstools/postcss-position-area-property@1.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q=="], + + "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@4.2.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw=="], + + "@csstools/postcss-property-rule-prelude-list": ["@csstools/postcss-property-rule-prelude-list@1.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA=="], + + "@csstools/postcss-random-function": ["@csstools/postcss-random-function@2.0.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w=="], + + "@csstools/postcss-relative-color-syntax": ["@csstools/postcss-relative-color-syntax@3.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw=="], + + "@csstools/postcss-scope-pseudo-class": ["@csstools/postcss-scope-pseudo-class@4.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q=="], + + "@csstools/postcss-sign-functions": ["@csstools/postcss-sign-functions@1.1.4", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg=="], + + "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@4.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA=="], + + "@csstools/postcss-syntax-descriptor-syntax-production": ["@csstools/postcss-syntax-descriptor-syntax-production@1.0.1", "", { "dependencies": { "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow=="], + + "@csstools/postcss-system-ui-font-family": ["@csstools/postcss-system-ui-font-family@1.0.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ=="], + + "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@4.0.3", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA=="], + + "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@4.0.9", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A=="], + + "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@4.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA=="], + + "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@3.1.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g=="], + + "@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="], + + "@csstools/utilities": ["@csstools/utilities@2.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ=="], + + "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], + + "@docsearch/core": ["@docsearch/core@4.6.3", "", { "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", "react": ">= 16.8.0 < 20.0.0", "react-dom": ">= 16.8.0 < 20.0.0" }, "optionalPeers": ["@types/react", "react", "react-dom"] }, "sha512-rUOujwIpxJRgD7+kicVsI3D5sqBvdiRTquzWBpTEXZs8ZXfGbfzpus5HqumaNYTppN2HvH8E2yNuRwYdHJeOlA=="], + + "@docsearch/css": ["@docsearch/css@4.6.3", "", {}, "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ=="], + + "@docsearch/react": ["@docsearch/react@4.6.3", "", { "dependencies": { "@algolia/autocomplete-core": "1.19.2", "@docsearch/core": "4.6.3", "@docsearch/css": "4.6.3" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", "react": ">= 16.8.0 < 20.0.0", "react-dom": ">= 16.8.0 < 20.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-Bg2wdDsoQVlNCcEKuEJAU04tvHCqgx8rIu+uIoM4pRtcx3TBKJuXutJik3LTA8LRc9YEyHkrYUrmcC0D7BYf+g=="], + + "@docusaurus/babel": ["@docusaurus/babel@3.10.1", "", { "dependencies": { "@babel/core": "^7.25.9", "@babel/generator": "^7.25.9", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.25.9", "@babel/preset-react": "^7.25.9", "@babel/preset-typescript": "^7.25.9", "@babel/runtime": "^7.25.9", "@babel/traverse": "^7.25.9", "@docusaurus/logger": "3.10.1", "@docusaurus/utils": "3.10.1", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" } }, "sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg=="], + + "@docusaurus/bundler": ["@docusaurus/bundler@3.10.1", "", { "dependencies": { "@babel/core": "^7.25.9", "@docusaurus/babel": "3.10.1", "@docusaurus/cssnano-preset": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "babel-loader": "^9.2.1", "clean-css": "^5.3.3", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.11.0", "css-minimizer-webpack-plugin": "^5.0.1", "cssnano": "^6.1.2", "file-loader": "^6.2.0", "html-minifier-terser": "^7.2.0", "mini-css-extract-plugin": "^2.9.2", "null-loader": "^4.0.1", "postcss": "^8.5.4", "postcss-loader": "^7.3.4", "postcss-preset-env": "^10.2.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", "webpack": "^5.95.0", "webpackbar": "^7.0.0" }, "peerDependencies": { "@docusaurus/faster": "*" }, "optionalPeers": ["@docusaurus/faster"] }, "sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw=="], + + "@docusaurus/core": ["@docusaurus/core@3.10.1", "", { "dependencies": { "@docusaurus/babel": "3.10.1", "@docusaurus/bundler": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", "cli-table3": "^0.6.3", "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", "execa": "^5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.3", "react-router": "^5.3.4", "react-router-config": "^5.1.1", "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.7", "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" }, "peerDependencies": { "@docusaurus/faster": "*", "@mdx-js/react": "^3.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@docusaurus/faster"], "bin": { "docusaurus": "bin/docusaurus.mjs" } }, "sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w=="], + + "@docusaurus/cssnano-preset": ["@docusaurus/cssnano-preset@3.10.1", "", { "dependencies": { "cssnano-preset-advanced": "^6.1.2", "postcss": "^8.5.4", "postcss-sort-media-queries": "^5.2.0", "tslib": "^2.6.0" } }, "sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ=="], + + "@docusaurus/logger": ["@docusaurus/logger@3.10.1", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.6.0" } }, "sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw=="], + + "@docusaurus/mdx-loader": ["@docusaurus/mdx-loader@3.10.1", "", { "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", "remark-directive": "^3.0.0", "remark-emoji": "^4.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "stringify-object": "^3.3.0", "tslib": "^2.6.0", "unified": "^11.0.3", "unist-util-visit": "^5.0.0", "url-loader": "^4.1.1", "vfile": "^6.0.1", "webpack": "^5.88.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ=="], + + "@docusaurus/module-type-aliases": ["@docusaurus/module-type-aliases@3.10.1", "", { "dependencies": { "@docusaurus/types": "3.10.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w=="], + + "@docusaurus/plugin-content-blog": ["@docusaurus/plugin-content-blog@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/theme-common": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "cheerio": "1.0.0-rc.12", "combine-promises": "^1.1.0", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-mmkgE6Q2+K74tnkou7tXlpDLvoCU/qkSa2GSQ3XUiHWvcebCoDQzS670RR3tO8PmaWlIyWWISYWzZLuMfxunRA=="], + + "@docusaurus/plugin-content-docs": ["@docusaurus/plugin-content-docs@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", "@docusaurus/theme-common": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ=="], + + "@docusaurus/plugin-content-pages": ["@docusaurus/plugin-content-pages@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-huJpaRPMl42nsFwuCXvV8bVDj2MazuwRJIUylI/RSlmZeJssVoZXeCjVf1y+1Drtpa9SKcdGn8yoJ76IRJijtw=="], + + "@docusaurus/plugin-css-cascade-layers": ["@docusaurus/plugin-css-cascade-layers@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "tslib": "^2.6.0" } }, "sha512-r//fn+MNHkE1wCof8T29VAQezt1enGCpsFxoziBbvLgBM4JfXN2P3rxrBaavHmvLvm7lYkpJeitcDthwnmWCTw=="], + + "@docusaurus/plugin-debug": ["@docusaurus/plugin-debug@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "fs-extra": "^11.1.1", "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-9KqOpKNfAyqGZykRb9LhIT/vyRF6sm/ykhjj/39JvaJahDS+jZJE0Z1Wfz9q3DUNDTMNN0Q7u/kk4rKKU+IJuA=="], + + "@docusaurus/plugin-google-analytics": ["@docusaurus/plugin-google-analytics@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "tslib": "^2.6.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-8o0P1KtmgdYQHH+oInitPpRWI0Of5XednAX4+DMhQNSmGSRNrsEEHg1ebv35m9AgRClfAytCJ5jA9KvcASTyuA=="], + + "@docusaurus/plugin-google-gtag": ["@docusaurus/plugin-google-gtag@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "@types/gtag.js": "^0.0.20", "tslib": "^2.6.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-pu3xIUo5o/zCMLfUY9BO5KOwSH0zIsAGyFRPvXHayFSA5XIhCU/SFuB0g0ZNjFn9niZLCaNvoeAuOGFJZq0fdw=="], + + "@docusaurus/plugin-google-tag-manager": ["@docusaurus/plugin-google-tag-manager@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "tslib": "^2.6.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-f6fyGHiCm7kJHBtAisGQS5oNBnpnMTYQZxDXeVrnw/3zWU+LMA22pr6UHGYkBKDbN+qPC5QHG3NuOfzQLq3+Lw=="], + + "@docusaurus/plugin-sitemap": ["@docusaurus/plugin-sitemap@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-C26MbmmqgdjkDq1htaZ3aD7LzEDKFWXfpyQpt0EOUThuq5nV77zDaedV20yHcVo9p+3ey9aZ4pbHA0D3QcZTzg=="], + + "@docusaurus/plugin-svgr": ["@docusaurus/plugin-svgr@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", "webpack": "^5.88.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-6SFxsmjWFkVLDmBUvFK6i72QjUwqyQFe4Ovz+SUJophJjOyVG3ZZG5IQpBC/kX/Gfv1yWeU9nWauH6F6Q7QX/Q=="], + + "@docusaurus/preset-classic": ["@docusaurus/preset-classic@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/plugin-content-blog": "3.10.1", "@docusaurus/plugin-content-docs": "3.10.1", "@docusaurus/plugin-content-pages": "3.10.1", "@docusaurus/plugin-css-cascade-layers": "3.10.1", "@docusaurus/plugin-debug": "3.10.1", "@docusaurus/plugin-google-analytics": "3.10.1", "@docusaurus/plugin-google-gtag": "3.10.1", "@docusaurus/plugin-google-tag-manager": "3.10.1", "@docusaurus/plugin-sitemap": "3.10.1", "@docusaurus/plugin-svgr": "3.10.1", "@docusaurus/theme-classic": "3.10.1", "@docusaurus/theme-common": "3.10.1", "@docusaurus/theme-search-algolia": "3.10.1", "@docusaurus/types": "3.10.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-YO/FL8v1zmbxoTso6mjMz/RDjhaTJxb1UpFFTDdY5847LLDCeyYiYlrhyTbgN1RIN3xnkLKZ9Lj1x8hUzI4JOg=="], + + "@docusaurus/theme-classic": ["@docusaurus/theme-classic@3.10.1", "", { "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", "@docusaurus/plugin-content-blog": "3.10.1", "@docusaurus/plugin-content-docs": "3.10.1", "@docusaurus/plugin-content-pages": "3.10.1", "@docusaurus/theme-common": "3.10.1", "@docusaurus/theme-translations": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", "nprogress": "^0.2.0", "postcss": "^8.5.4", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react-router-dom": "^5.3.4", "rtlcss": "^4.1.0", "tslib": "^2.6.0", "utility-types": "^3.10.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-VU1RK0qb2pab0si4r7HFK37cYco8VzqLj3u1PspVipSr/z/GPVKHO4/HXbnePqHoWDk8urjyGSeatH0NIMBM1A=="], + + "@docusaurus/theme-common": ["@docusaurus/theme-common@3.10.1", "", { "dependencies": { "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "clsx": "^2.0.0", "parse-numeric-range": "^1.3.0", "prism-react-renderer": "^2.3.0", "tslib": "^2.6.0", "utility-types": "^3.10.0" }, "peerDependencies": { "@docusaurus/plugin-content-docs": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA=="], + + "@docusaurus/theme-search-algolia": ["@docusaurus/theme-search-algolia@3.10.1", "", { "dependencies": { "@algolia/autocomplete-core": "^1.19.2", "@docsearch/react": "^3.9.0 || ^4.3.2", "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", "@docusaurus/plugin-content-docs": "3.10.1", "@docusaurus/theme-common": "3.10.1", "@docusaurus/theme-translations": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-validation": "3.10.1", "algoliasearch": "^5.37.0", "algoliasearch-helper": "^3.26.0", "clsx": "^2.0.0", "eta": "^2.2.0", "fs-extra": "^11.1.1", "lodash": "^4.17.21", "tslib": "^2.6.0", "utility-types": "^3.10.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OTaARARVZj2GvkJQjB+1jOIxntRaXea+G+fMsNqrZBAU1O1vJKDW22R7kECOHW27oJCLFN9HKaZeRrfAUyviug=="], + + "@docusaurus/theme-translations": ["@docusaurus/theme-translations@3.10.1", "", { "dependencies": { "fs-extra": "^11.1.1", "tslib": "^2.6.0" } }, "sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw=="], + + "@docusaurus/types": ["@docusaurus/types@3.10.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@types/history": "^4.7.11", "@types/mdast": "^4.0.2", "@types/react": "*", "commander": "^5.1.0", "joi": "^17.9.2", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "utility-types": "^3.10.0", "webpack": "^5.95.0", "webpack-merge": "^5.9.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw=="], + + "@docusaurus/utils": ["@docusaurus/utils@3.10.1", "", { "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/types": "3.10.1", "@docusaurus/utils-common": "3.10.1", "escape-string-regexp": "^4.0.0", "execa": "^5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", "globby": "^11.1.0", "gray-matter": "^4.0.3", "jiti": "^1.20.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", "webpack": "^5.88.1" } }, "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw=="], + + "@docusaurus/utils-common": ["@docusaurus/utils-common@3.10.1", "", { "dependencies": { "@docusaurus/types": "3.10.1", "tslib": "^2.6.0" } }, "sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA=="], + + "@docusaurus/utils-validation": ["@docusaurus/utils-validation@3.10.1", "", { "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/utils": "3.10.1", "@docusaurus/utils-common": "3.10.1", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "tslib": "^2.6.0" } }, "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jsonjoy.com/base64": ["@jsonjoy.com/base64@1.1.2", "", { "peerDependencies": { "tslib": "2" } }, "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA=="], + + "@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw=="], + + "@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@1.0.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g=="], + + "@jsonjoy.com/fs-core": ["@jsonjoy.com/fs-core@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew=="], + + "@jsonjoy.com/fs-fsa": ["@jsonjoy.com/fs-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w=="], + + "@jsonjoy.com/fs-node": ["@jsonjoy.com/fs-node@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA=="], + + "@jsonjoy.com/fs-node-builtins": ["@jsonjoy.com/fs-node-builtins@4.64.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA=="], + + "@jsonjoy.com/fs-node-to-fsa": ["@jsonjoy.com/fs-node-to-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw=="], + + "@jsonjoy.com/fs-node-utils": ["@jsonjoy.com/fs-node-utils@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "glob-to-regex.js": "^1.0.1" }, "peerDependencies": { "tslib": "2" } }, "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w=="], + + "@jsonjoy.com/fs-print": ["@jsonjoy.com/fs-print@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-utils": "4.64.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw=="], + + "@jsonjoy.com/fs-snapshot": ["@jsonjoy.com/fs-snapshot@4.64.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q=="], + + "@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@1.21.0", "", { "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg=="], + + "@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@1.0.2", "", { "dependencies": { "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg=="], + + "@jsonjoy.com/util": ["@jsonjoy.com/util@1.9.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ=="], + + "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + + "@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-rsa": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA=="], + + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pfx": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA=="], + + "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], + + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], + + "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], + + "@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.3", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + + "@slorber/remark-comment": ["@slorber/remark-comment@1.0.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.1.0", "micromark-util-symbol": "^1.0.1" } }, "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA=="], + + "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], + + "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], + + "@svgr/babel-plugin-remove-jsx-empty-expression": ["@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA=="], + + "@svgr/babel-plugin-replace-jsx-attribute-value": ["@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ=="], + + "@svgr/babel-plugin-svg-dynamic-title": ["@svgr/babel-plugin-svg-dynamic-title@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og=="], + + "@svgr/babel-plugin-svg-em-dimensions": ["@svgr/babel-plugin-svg-em-dimensions@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g=="], + + "@svgr/babel-plugin-transform-react-native-svg": ["@svgr/babel-plugin-transform-react-native-svg@8.1.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q=="], + + "@svgr/babel-plugin-transform-svg-component": ["@svgr/babel-plugin-transform-svg-component@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw=="], + + "@svgr/babel-preset": ["@svgr/babel-preset@8.1.0", "", { "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug=="], + + "@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], + + "@svgr/hast-util-to-babel-ast": ["@svgr/hast-util-to-babel-ast@8.0.0", "", { "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" } }, "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q=="], + + "@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], + + "@svgr/plugin-svgo": ["@svgr/plugin-svgo@8.1.0", "", { "dependencies": { "cosmiconfig": "^8.1.3", "deepmerge": "^4.3.1", "svgo": "^3.0.2" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA=="], + + "@svgr/webpack": ["@svgr/webpack@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@babel/plugin-transform-react-constant-elements": "^7.21.3", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@svgr/core": "8.1.0", "@svgr/plugin-jsx": "8.1.0", "@svgr/plugin-svgo": "8.1.0" } }, "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA=="], + + "@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/connect-history-api-fallback": ["@types/connect-history-api-fallback@1.5.4", "", { "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="], + + "@types/gtag.js": ["@types/gtag.js@0.0.20", "", {}, "sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg=="], + + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + + "@types/history": ["@types/history@4.7.11", "", {}, "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="], + + "@types/html-minifier-terser": ["@types/html-minifier-terser@6.1.0", "", {}, "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="], + + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], + + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-router": ["@types/react-router@5.1.20", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" } }, "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q=="], + + "@types/react-router-config": ["@types/react-router-config@5.0.11", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "^5.1.0" } }, "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw=="], + + "@types/react-router-dom": ["@types/react-router-dom@5.3.3", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } }, "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="], + + "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], + + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@types/serve-index": ["@types/serve-index@1.9.4", "", { "dependencies": { "@types/express": "*" } }, "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + + "@types/sockjs": ["@types/sockjs@0.3.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], + + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "algoliasearch": ["algoliasearch@5.55.2", "", { "dependencies": { "@algolia/abtesting": "1.21.2", "@algolia/client-abtesting": "5.55.2", "@algolia/client-analytics": "5.55.2", "@algolia/client-common": "5.55.2", "@algolia/client-insights": "5.55.2", "@algolia/client-personalization": "5.55.2", "@algolia/client-query-suggestions": "5.55.2", "@algolia/client-search": "5.55.2", "@algolia/ingestion": "1.55.2", "@algolia/monitoring": "1.55.2", "@algolia/recommend": "5.55.2", "@algolia/requester-browser-xhr": "5.55.2", "@algolia/requester-fetch": "5.55.2", "@algolia/requester-node-http": "5.55.2" } }, "sha512-OyacJsaeuLUvGWOynNqYc6sx88XvyoG39wMT8SYqL3l9wwaorDW/LPRbUPfhzw0bWsUWzNCZTnFYOrWFBKsUaw=="], + + "algoliasearch-helper": ["algoliasearch-helper@3.29.2", "", { "dependencies": { "@algolia/events": "^4.0.1" }, "peerDependencies": { "algoliasearch": ">= 3.1 < 6" } }, "sha512-SaV+rZM3drExb0punEYYjT+sNcH74YFwN8ocjya7IDOyQvKWeQpEaSMVG3+IGTVos+feuatj7ljQ4BXlXdUp3w=="], + + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + + "ansi-html-community": ["ansi-html-community@0.0.8", "", { "bin": { "ansi-html": "bin/ansi-html" } }, "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "autoprefixer": ["autoprefixer@10.5.2", "", { "dependencies": { "browserslist": "^4.28.4", "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q=="], + + "babel-loader": ["babel-loader@9.2.1", "", { "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "peerDependencies": { "@babel/core": "^7.12.0", "webpack": ">=5" } }, "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA=="], + + "babel-plugin-dynamic-import-node": ["babel-plugin-dynamic-import-node@2.3.3", "", { "dependencies": { "object.assign": "^4.1.0" } }, "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ=="], + + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="], + + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], + + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.42", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q=="], + + "batch": ["batch@0.6.1", "", {}, "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="], + + "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + + "bonjour-service": ["bonjour-service@1.4.2", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-lMskhnsW70yWHr4PhPeh2rvaIkLSaDpp+nmtbXBZaNKTXwxL73QOkW6HhbzqTImXjevn9TreGT4GACGBCGP9nQ=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "boxen": ["boxen@6.2.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^6.2.0", "chalk": "^4.1.2", "cli-boxes": "^3.0.0", "string-width": "^5.0.1", "type-fest": "^2.5.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw=="], + + "brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.5", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", "electron-to-chromium": "^1.5.387", "node-releases": "^2.0.50", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], + + "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], + + "cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="], + + "cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001803", "", {}, "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "cheerio": ["cheerio@1.0.0-rc.12", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], + + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + + "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "combine-promises": ["combine-promises@1.2.0", "", {}, "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], + + "configstore": ["configstore@6.0.0", "", { "dependencies": { "dot-prop": "^6.0.1", "graceful-fs": "^4.2.6", "unique-string": "^3.0.0", "write-file-atomic": "^3.0.3", "xdg-basedir": "^5.0.1" } }, "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA=="], + + "connect-history-api-fallback": ["connect-history-api-fallback@2.0.0", "", {}, "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "copy-text-to-clipboard": ["copy-text-to-clipboard@3.2.2", "", {}, "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A=="], + + "copy-webpack-plugin": ["copy-webpack-plugin@11.0.0", "", { "dependencies": { "fast-glob": "^3.2.11", "glob-parent": "^6.0.1", "globby": "^13.1.1", "normalize-path": "^3.0.0", "schema-utils": "^4.0.0", "serialize-javascript": "^6.0.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ=="], + + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + + "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], + + "css-blank-pseudo": ["css-blank-pseudo@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag=="], + + "css-declaration-sorter": ["css-declaration-sorter@7.4.0", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw=="], + + "css-has-pseudo": ["css-has-pseudo@7.0.3", "", { "dependencies": { "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA=="], + + "css-loader": ["css-loader@6.11.0", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g=="], + + "css-minimizer-webpack-plugin": ["css-minimizer-webpack-plugin@5.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "cssnano": "^6.0.1", "jest-worker": "^29.4.3", "postcss": "^8.4.24", "schema-utils": "^4.0.1", "serialize-javascript": "^6.0.1" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg=="], + + "css-prefers-color-scheme": ["css-prefers-color-scheme@10.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ=="], + + "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + + "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "cssdb": ["cssdb@8.9.0", "", {}, "sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssnano": ["cssnano@6.1.2", "", { "dependencies": { "cssnano-preset-default": "^6.1.2", "lilconfig": "^3.1.1" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA=="], + + "cssnano-preset-advanced": ["cssnano-preset-advanced@6.1.2", "", { "dependencies": { "autoprefixer": "^10.4.19", "browserslist": "^4.23.0", "cssnano-preset-default": "^6.1.2", "postcss-discard-unused": "^6.0.5", "postcss-merge-idents": "^6.0.3", "postcss-reduce-idents": "^6.0.3", "postcss-zindex": "^6.0.2" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ=="], + + "cssnano-preset-default": ["cssnano-preset-default@6.1.2", "", { "dependencies": { "browserslist": "^4.23.0", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^4.0.2", "postcss-calc": "^9.0.1", "postcss-colormin": "^6.1.0", "postcss-convert-values": "^6.1.0", "postcss-discard-comments": "^6.0.2", "postcss-discard-duplicates": "^6.0.3", "postcss-discard-empty": "^6.0.3", "postcss-discard-overridden": "^6.0.2", "postcss-merge-longhand": "^6.0.5", "postcss-merge-rules": "^6.1.1", "postcss-minify-font-values": "^6.1.0", "postcss-minify-gradients": "^6.0.3", "postcss-minify-params": "^6.1.0", "postcss-minify-selectors": "^6.0.4", "postcss-normalize-charset": "^6.0.2", "postcss-normalize-display-values": "^6.0.2", "postcss-normalize-positions": "^6.0.2", "postcss-normalize-repeat-style": "^6.0.2", "postcss-normalize-string": "^6.0.2", "postcss-normalize-timing-functions": "^6.0.2", "postcss-normalize-unicode": "^6.1.0", "postcss-normalize-url": "^6.0.2", "postcss-normalize-whitespace": "^6.0.2", "postcss-ordered-values": "^6.0.2", "postcss-reduce-initial": "^6.1.0", "postcss-reduce-transforms": "^6.0.2", "postcss-svgo": "^6.0.3", "postcss-unique-selectors": "^6.0.4" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg=="], + + "cssnano-utils": ["cssnano-utils@4.0.2", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + + "detect-port": ["detect-port@1.6.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="], + + "dom-converter": ["dom-converter@0.2.0", "", { "dependencies": { "utila": "~0.4" } }, "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.389", "", {}, "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], + + "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="], + + "emoticon": ["emoticon@4.1.0", "", {}, "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.24.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.3.0", "", {}, "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eta": ["eta@2.2.0", "", {}, "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eval": ["eval@0.1.8", "", { "dependencies": { "@types/node": "*", "require-like": ">= 0.1.1" } }, "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + + "faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="], + + "feed": ["feed@4.2.2", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ=="], + + "file-loader": ["file-loader@6.2.0", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], + + "find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], + + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fs-extra": ["fs-extra@11.3.6", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "github-slugger": ["github-slugger@1.5.0", "", {}, "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob-to-regex.js": ["glob-to-regex.js@1.2.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ=="], + + "global-dirs": ["global-dirs@3.0.1", "", { "dependencies": { "ini": "2.0.0" } }, "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + + "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], + + "handle-thing": ["handle-thing@2.0.1", "", {}, "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-yarn": ["has-yarn@3.0.0", "", {}, "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "history": ["history@4.10.1", "", { "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", "resolve-pathname": "^3.0.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0", "value-equal": "^1.0.1" } }, "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "hpack.js": ["hpack.js@2.1.6", "", { "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", "readable-stream": "^2.0.1", "wbuf": "^1.1.0" } }, "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "html-minifier-terser": ["html-minifier-terser@7.2.0", "", { "dependencies": { "camel-case": "^4.1.2", "clean-css": "~5.3.2", "commander": "^10.0.0", "entities": "^4.4.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.15.1" }, "bin": { "html-minifier-terser": "cli.js" } }, "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA=="], + + "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "html-webpack-plugin": ["html-webpack-plugin@5.6.7", "", { "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", "lodash": "^4.17.21", "pretty-error": "^4.0.0", "tapable": "^2.0.0" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw=="], + + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-deceiver": ["http-deceiver@1.2.7", "", {}, "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "http-proxy-middleware": ["http-proxy-middleware@2.0.10", "", { "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "peerDependencies": { "@types/express": "^4.17.13" }, "optionalPeers": ["@types/express"] }, "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ=="], + + "http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="], + + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "hyperdyperid": ["hyperdyperid@1.2.0", "", {}, "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "infima": ["infima@0.2.0-alpha.45", "", {}, "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + + "ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-installed-globally": ["is-installed-globally@0.4.0", "", { "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" } }, "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ=="], + + "is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="], + + "is-npm": ["is-npm@6.1.0", "", {}, "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "is-yarn-global": ["is-yarn-global@0.4.1", "", {}, "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ=="], + + "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "joi": ["joi@17.13.4", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "latest-version": ["latest-version@7.0.0", "", { "dependencies": { "package-json": "^8.1.0" } }, "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg=="], + + "launch-editor": ["launch-editor@2.14.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.4" } }, "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], + + "loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], + + "locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "memfs": ["memfs@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-to-fsa": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@1.1.0", "", {}, "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="], + + "mini-css-extract-plugin": ["mini-css-extract-plugin@2.10.2", "", { "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], + + "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-emoji": ["node-emoji@2.2.0", "", { "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", "emojilib": "^2.4.0", "skin-tone": "^2.0.0" } }, "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw=="], + + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-url": ["normalize-url@8.1.1", "", {}, "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ=="], + + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nprogress": ["nprogress@0.2.0", "", {}, "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "null-loader": ["null-loader@4.0.1", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], + + "p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="], + + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + + "p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], + + "p-retry": ["p-retry@6.2.1", "", { "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" } }, "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ=="], + + "p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], + + "package-json": ["package-json@8.1.1", "", { "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", "registry-url": "^6.0.0", "semver": "^7.3.7" } }, "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA=="], + + "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-numeric-range": ["parse-numeric-range@1.3.0", "", {}, "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + + "path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "path-is-inside": ["path-is-inside@1.0.2", "", {}, "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], + + "pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="], + + "postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="], + + "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@7.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw=="], + + "postcss-calc": ["postcss-calc@9.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ=="], + + "postcss-clamp": ["postcss-clamp@4.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.6" } }, "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow=="], + + "postcss-color-functional-notation": ["postcss-color-functional-notation@7.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw=="], + + "postcss-color-hex-alpha": ["postcss-color-hex-alpha@10.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w=="], + + "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@10.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ=="], + + "postcss-colormin": ["postcss-colormin@6.1.0", "", { "dependencies": { "browserslist": "^4.23.0", "caniuse-api": "^3.0.0", "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw=="], + + "postcss-convert-values": ["postcss-convert-values@6.1.0", "", { "dependencies": { "browserslist": "^4.23.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w=="], + + "postcss-custom-media": ["postcss-custom-media@11.0.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw=="], + + "postcss-custom-properties": ["postcss-custom-properties@14.0.6", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ=="], + + "postcss-custom-selectors": ["postcss-custom-selectors@8.0.5", "", { "dependencies": { "@csstools/cascade-layer-name-parser": "^2.0.5", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg=="], + + "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@9.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA=="], + + "postcss-discard-comments": ["postcss-discard-comments@6.0.2", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw=="], + + "postcss-discard-duplicates": ["postcss-discard-duplicates@6.0.3", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw=="], + + "postcss-discard-empty": ["postcss-discard-empty@6.0.3", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ=="], + + "postcss-discard-overridden": ["postcss-discard-overridden@6.0.2", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ=="], + + "postcss-discard-unused": ["postcss-discard-unused@6.0.5", "", { "dependencies": { "postcss-selector-parser": "^6.0.16" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA=="], + + "postcss-double-position-gradients": ["postcss-double-position-gradients@6.0.4", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g=="], + + "postcss-focus-visible": ["postcss-focus-visible@10.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA=="], + + "postcss-focus-within": ["postcss-focus-within@9.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw=="], + + "postcss-font-variant": ["postcss-font-variant@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA=="], + + "postcss-gap-properties": ["postcss-gap-properties@6.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw=="], + + "postcss-image-set-function": ["postcss-image-set-function@7.0.0", "", { "dependencies": { "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA=="], + + "postcss-lab-function": ["postcss-lab-function@7.0.12", "", { "dependencies": { "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/utilities": "^2.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w=="], + + "postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], + + "postcss-logical": ["postcss-logical@8.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA=="], + + "postcss-merge-idents": ["postcss-merge-idents@6.0.3", "", { "dependencies": { "cssnano-utils": "^4.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g=="], + + "postcss-merge-longhand": ["postcss-merge-longhand@6.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^6.1.1" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w=="], + + "postcss-merge-rules": ["postcss-merge-rules@6.1.1", "", { "dependencies": { "browserslist": "^4.23.0", "caniuse-api": "^3.0.0", "cssnano-utils": "^4.0.2", "postcss-selector-parser": "^6.0.16" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ=="], + + "postcss-minify-font-values": ["postcss-minify-font-values@6.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg=="], + + "postcss-minify-gradients": ["postcss-minify-gradients@6.0.3", "", { "dependencies": { "colord": "^2.9.3", "cssnano-utils": "^4.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q=="], + + "postcss-minify-params": ["postcss-minify-params@6.1.0", "", { "dependencies": { "browserslist": "^4.23.0", "cssnano-utils": "^4.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA=="], + + "postcss-minify-selectors": ["postcss-minify-selectors@6.0.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.16" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ=="], + + "postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="], + + "postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="], + + "postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="], + + "postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="], + + "postcss-nesting": ["postcss-nesting@13.0.2", "", { "dependencies": { "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ=="], + + "postcss-normalize-charset": ["postcss-normalize-charset@6.0.2", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ=="], + + "postcss-normalize-display-values": ["postcss-normalize-display-values@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg=="], + + "postcss-normalize-positions": ["postcss-normalize-positions@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q=="], + + "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ=="], + + "postcss-normalize-string": ["postcss-normalize-string@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ=="], + + "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA=="], + + "postcss-normalize-unicode": ["postcss-normalize-unicode@6.1.0", "", { "dependencies": { "browserslist": "^4.23.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg=="], + + "postcss-normalize-url": ["postcss-normalize-url@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ=="], + + "postcss-normalize-whitespace": ["postcss-normalize-whitespace@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q=="], + + "postcss-opacity-percentage": ["postcss-opacity-percentage@3.0.0", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ=="], + + "postcss-ordered-values": ["postcss-ordered-values@6.0.2", "", { "dependencies": { "cssnano-utils": "^4.0.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q=="], + + "postcss-overflow-shorthand": ["postcss-overflow-shorthand@6.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q=="], + + "postcss-page-break": ["postcss-page-break@3.0.4", "", { "peerDependencies": { "postcss": "^8" } }, "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ=="], + + "postcss-place": ["postcss-place@10.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw=="], + + "postcss-preset-env": ["postcss-preset-env@10.6.1", "", { "dependencies": { "@csstools/postcss-alpha-function": "^1.0.1", "@csstools/postcss-cascade-layers": "^5.0.2", "@csstools/postcss-color-function": "^4.0.12", "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", "@csstools/postcss-color-mix-function": "^3.0.12", "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", "@csstools/postcss-content-alt-text": "^2.0.8", "@csstools/postcss-contrast-color-function": "^2.0.12", "@csstools/postcss-exponential-functions": "^2.0.9", "@csstools/postcss-font-format-keywords": "^4.0.0", "@csstools/postcss-gamut-mapping": "^2.0.11", "@csstools/postcss-gradients-interpolation-method": "^5.0.12", "@csstools/postcss-hwb-function": "^4.0.12", "@csstools/postcss-ic-unit": "^4.0.4", "@csstools/postcss-initial": "^2.0.1", "@csstools/postcss-is-pseudo-class": "^5.0.3", "@csstools/postcss-light-dark-function": "^2.0.11", "@csstools/postcss-logical-float-and-clear": "^3.0.0", "@csstools/postcss-logical-overflow": "^2.0.0", "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", "@csstools/postcss-logical-resize": "^3.0.0", "@csstools/postcss-logical-viewport-units": "^3.0.4", "@csstools/postcss-media-minmax": "^2.0.9", "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", "@csstools/postcss-nested-calc": "^4.0.0", "@csstools/postcss-normalize-display-values": "^4.0.1", "@csstools/postcss-oklab-function": "^4.0.12", "@csstools/postcss-position-area-property": "^1.0.0", "@csstools/postcss-progressive-custom-properties": "^4.2.1", "@csstools/postcss-property-rule-prelude-list": "^1.0.0", "@csstools/postcss-random-function": "^2.0.1", "@csstools/postcss-relative-color-syntax": "^3.0.12", "@csstools/postcss-scope-pseudo-class": "^4.0.1", "@csstools/postcss-sign-functions": "^1.1.4", "@csstools/postcss-stepped-value-functions": "^4.0.9", "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", "@csstools/postcss-system-ui-font-family": "^1.0.0", "@csstools/postcss-text-decoration-shorthand": "^4.0.3", "@csstools/postcss-trigonometric-functions": "^4.0.9", "@csstools/postcss-unset-value": "^4.0.0", "autoprefixer": "^10.4.23", "browserslist": "^4.28.1", "css-blank-pseudo": "^7.0.1", "css-has-pseudo": "^7.0.3", "css-prefers-color-scheme": "^10.0.0", "cssdb": "^8.6.0", "postcss-attribute-case-insensitive": "^7.0.1", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^7.0.12", "postcss-color-hex-alpha": "^10.0.0", "postcss-color-rebeccapurple": "^10.0.0", "postcss-custom-media": "^11.0.6", "postcss-custom-properties": "^14.0.6", "postcss-custom-selectors": "^8.0.5", "postcss-dir-pseudo-class": "^9.0.1", "postcss-double-position-gradients": "^6.0.4", "postcss-focus-visible": "^10.0.1", "postcss-focus-within": "^9.0.1", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^6.0.0", "postcss-image-set-function": "^7.0.0", "postcss-lab-function": "^7.0.12", "postcss-logical": "^8.1.0", "postcss-nesting": "^13.0.2", "postcss-opacity-percentage": "^3.0.0", "postcss-overflow-shorthand": "^6.0.0", "postcss-page-break": "^3.0.4", "postcss-place": "^10.0.0", "postcss-pseudo-class-any-link": "^10.0.1", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^8.0.1" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g=="], + + "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@10.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q=="], + + "postcss-reduce-idents": ["postcss-reduce-idents@6.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA=="], + + "postcss-reduce-initial": ["postcss-reduce-initial@6.1.0", "", { "dependencies": { "browserslist": "^4.23.0", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw=="], + + "postcss-reduce-transforms": ["postcss-reduce-transforms@6.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA=="], + + "postcss-replace-overflow-wrap": ["postcss-replace-overflow-wrap@4.0.0", "", { "peerDependencies": { "postcss": "^8.0.3" } }, "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw=="], + + "postcss-selector-not": ["postcss-selector-not@8.0.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], + + "postcss-sort-media-queries": ["postcss-sort-media-queries@5.2.0", "", { "dependencies": { "sort-css-media-queries": "2.2.0" }, "peerDependencies": { "postcss": "^8.4.23" } }, "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA=="], + + "postcss-svgo": ["postcss-svgo@6.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^3.2.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g=="], + + "postcss-unique-selectors": ["postcss-unique-selectors@6.0.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.16" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "postcss-zindex": ["postcss-zindex@6.0.2", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg=="], + + "pretty-error": ["pretty-error@4.0.0", "", { "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" } }, "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw=="], + + "pretty-time": ["pretty-time@1.1.0", "", {}, "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA=="], + + "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + + "react-helmet-async": ["@slorber/react-helmet-async@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "invariant": "^2.2.4", "prop-types": "^15.7.2", "react-fast-compare": "^3.2.0", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-json-view-lite": ["react-json-view-lite@2.5.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g=="], + + "react-loadable": ["@docusaurus/react-loadable@6.0.0", "", { "dependencies": { "@types/react": "*" }, "peerDependencies": { "react": "*" } }, "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ=="], + + "react-loadable-ssr-addon-v5-slorber": ["react-loadable-ssr-addon-v5-slorber@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.10.3" }, "peerDependencies": { "react-loadable": "*", "webpack": ">=4.41.1 || 5.x" } }, "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ=="], + + "react-router": ["react-router@5.3.4", "", { "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", "path-to-regexp": "^1.7.0", "prop-types": "^15.6.2", "react-is": "^16.6.0", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" }, "peerDependencies": { "react": ">=15" } }, "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA=="], + + "react-router-config": ["react-router-config@5.1.1", "", { "dependencies": { "@babel/runtime": "^7.1.2" }, "peerDependencies": { "react": ">=15", "react-router": ">=5" } }, "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg=="], + + "react-router-dom": ["react-router-dom@5.3.4", "", { "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", "loose-envify": "^1.3.1", "prop-types": "^15.6.2", "react-router": "5.3.4", "tiny-invariant": "^1.0.2", "tiny-warning": "^1.0.0" }, "peerDependencies": { "react": ">=15" } }, "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], + + "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], + + "registry-auth-token": ["registry-auth-token@5.1.1", "", { "dependencies": { "@pnpm/npm-conf": "^3.0.2" } }, "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q=="], + + "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], + + "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], + + "regjsparser": ["regjsparser@0.13.2", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], + + "remark-emoji": ["remark-emoji@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.2", "emoticon": "^4.0.1", "mdast-util-find-and-replace": "^3.0.1", "node-emoji": "^2.1.0", "unified": "^11.0.4" } }, "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg=="], + + "remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "renderkid": ["renderkid@3.0.0", "", { "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "require-like": ["require-like@0.1.2", "", {}, "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pathname": ["resolve-pathname@3.0.0", "", {}, "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="], + + "responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="], + + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rtlcss": ["rtlcss@4.3.0", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0", "postcss": "^8.4.21", "strip-json-comments": "^3.1.1" }, "bin": { "rtlcss": "bin/rtlcss.js" } }, "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "schema-dts": ["schema-dts@1.1.5", "", {}, "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg=="], + + "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + + "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="], + + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "select-hose": ["select-hose@2.0.0", "", {}, "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg=="], + + "selfsigned": ["selfsigned@5.5.0", "", { "dependencies": { "@peculiar/x509": "^1.14.2", "pkijs": "^3.3.3" } }, "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "semver-diff": ["semver-diff@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "serve-handler": ["serve-handler@6.1.7", "", { "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg=="], + + "serve-index": ["serve-index@1.9.2", "", { "dependencies": { "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.8.0", "mime-types": "~2.1.35", "parseurl": "~1.3.3" } }, "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], + + "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.9.0", "", {}, "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sitemap": ["sitemap@7.1.3", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.2.4" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-tAjEd+wt/YwnEbfNB2ht51ybBJxbEWwe5ki/Z//Wh0rpBFTCUSj46GnxUKEWzhfuJTsee8x3lybHxFgUMig2hw=="], + + "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "sockjs": ["sockjs@0.3.24", "", { "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" } }, "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ=="], + + "sort-css-media-queries": ["sort-css-media-queries@2.2.0", "", {}, "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "spdy": ["spdy@4.0.2", "", { "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", "select-hose": "^2.0.0", "spdy-transport": "^3.0.0" } }, "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA=="], + + "spdy-transport": ["spdy-transport@3.0.0", "", { "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", "hpack.js": "^2.1.6", "obuf": "^1.1.2", "readable-stream": "^3.0.6", "wbuf": "^1.7.3" } }, "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "srcset": ["srcset@4.0.0", "", {}, "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "stylehacks": ["stylehacks@6.1.1", "", { "dependencies": { "browserslist": "^4.23.0", "postcss-selector-parser": "^6.0.16" }, "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + + "svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "terser": ["terser@5.49.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="], + + "thingies": ["thingies@2.6.0", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg=="], + + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tree-dump": ["tree-dump@1.1.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], + + "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], + + "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], + + "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], + + "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "update-notifier": ["update-notifier@6.0.2", "", { "dependencies": { "boxen": "^7.0.0", "chalk": "^5.0.1", "configstore": "^6.0.0", "has-yarn": "^3.0.0", "import-lazy": "^4.0.0", "is-ci": "^3.0.1", "is-installed-globally": "^0.4.0", "is-npm": "^6.0.0", "is-yarn-global": "^0.4.0", "latest-version": "^7.0.0", "pupa": "^3.1.0", "semver": "^7.3.7", "semver-diff": "^4.0.0", "xdg-basedir": "^5.1.0" } }, "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-loader": ["url-loader@4.1.1", "", { "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", "schema-utils": "^3.0.0" }, "peerDependencies": { "file-loader": "*", "webpack": "^4.0.0 || ^5.0.0" }, "optionalPeers": ["file-loader"] }, "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utila": ["utila@0.4.0", "", {}, "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="], + + "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "value-equal": ["value-equal@1.0.1", "", {}, "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="], + + "wbuf": ["wbuf@1.7.3", "", { "dependencies": { "minimalistic-assert": "^1.0.0" } }, "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "webpack": ["webpack@5.108.4", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.22.2", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "graceful-fs": "^4.2.11", "loader-runner": "^4.3.2", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "watchpack": "^2.5.2", "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w=="], + + "webpack-bundle-analyzer": ["webpack-bundle-analyzer@4.10.2", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw=="], + + "webpack-dev-middleware": ["webpack-dev-middleware@7.4.5", "", { "dependencies": { "colorette": "^2.0.10", "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"] }, "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA=="], + + "webpack-dev-server": ["webpack-dev-server@5.2.6", "", { "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", "@types/express": "^4.17.25", "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", "express": "^4.22.1", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", "launch-editor": "^2.14.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^7.4.2", "ws": "^8.18.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"], "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" } }, "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw=="], + + "webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="], + + "webpack-sources": ["webpack-sources@3.5.1", "", {}, "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw=="], + + "webpackbar": ["webpackbar@7.0.0", "", { "dependencies": { "ansis": "^3.2.0", "consola": "^3.2.3", "pretty-time": "^1.1.0", "std-env": "^3.7.0" }, "peerDependencies": { "@rspack/core": "*", "webpack": "3 || 4 || 5" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q=="], + + "websocket-driver": ["websocket-driver@0.7.5", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA=="], + + "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], + + "wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], + + "ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], + + "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.2", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g=="], + + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@docsearch/react/@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.19.2", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", "@algolia/autocomplete-shared": "1.19.2" } }, "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw=="], + + "@docusaurus/core/webpack-merge": ["webpack-merge@6.0.1", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.1" } }, "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@17.67.0", "", { "dependencies": { "@jsonjoy.com/base64": "17.67.0", "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0", "@jsonjoy.com/json-pointer": "17.67.0", "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util": ["@jsonjoy.com/util@17.67.0", "", { "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew=="], + + "@jsonjoy.com/json-pack/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + + "@jsonjoy.com/util/@jsonjoy.com/buffers": ["@jsonjoy.com/buffers@1.2.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA=="], + + "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "cheerio-select/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "cli-table3/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "compression/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "copy-webpack-plugin/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "copy-webpack-plugin/globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], + + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "css-minimizer-webpack-plugin/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "css-select/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "css-select/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "file-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], + + "gray-matter/js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="], + + "hpack.js/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], + + "html-webpack-plugin/html-minifier-terser": ["html-minifier-terser@6.1.0", "", { "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", "commander": "^8.3.0", "he": "^1.2.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" } }, "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw=="], + + "http-proxy-middleware/is-plain-obj": ["is-plain-obj@3.0.0", "", {}, "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="], + + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-from-markdown/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-gfm-autolink-literal/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-core-commonmark/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-core-commonmark/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-core-commonmark/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-directive/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-directive/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-directive/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-frontmatter/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-frontmatter/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-gfm-autolink-literal/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-gfm-autolink-literal/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-gfm-footnote/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-gfm-footnote/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-gfm-footnote/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-gfm-strikethrough/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-gfm-table/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-gfm-table/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-gfm-table/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-gfm-task-list-item/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-gfm-task-list-item/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-gfm-task-list-item/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-mdx-expression/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-mdx-expression/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-mdx-expression/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-mdx-jsx/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-extension-mdx-jsx/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-mdx-jsx/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-extension-mdxjs-esm/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-extension-mdxjs-esm/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-factory-destination/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-factory-destination/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-factory-label/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-factory-label/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-factory-mdx-expression/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-mdx-expression/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-factory-mdx-expression/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-factory-space/micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="], + + "micromark-factory-title/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-factory-title/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-factory-whitespace/micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-whitespace/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-factory-whitespace/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-character/micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="], + + "micromark-util-chunked/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-classify-character/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-classify-character/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-decode-numeric-character-reference/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-decode-string/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-decode-string/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-events-to-acorn/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-normalize-identifier/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-sanitize-uri/micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-sanitize-uri/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-subtokenize/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="], + + "null-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "postcss-calc/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-discard-unused/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-merge-rules/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-minify-selectors/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-unique-selectors/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "raw-body/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "renderkid/htmlparser2": ["htmlparser2@6.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="], + + "renderkid/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "serve-handler/path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="], + + "serve-index/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "serve-index/http-errors": ["http-errors@1.8.1", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.1" } }, "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g=="], + + "serve-index/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "stylehacks/postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "svgo/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "update-notifier/boxen": ["boxen@7.1.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0" } }, "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog=="], + + "update-notifier/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "url-loader/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "url-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "webpack-bundle-analyzer/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "webpack-dev-middleware/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "webpack-dev-middleware/range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "webpack-dev-server/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "webpack-dev-server/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "@docsearch/react/@algolia/autocomplete-core/@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.19.2", "", { "dependencies": { "@algolia/autocomplete-shared": "1.19.2" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg=="], + + "@docsearch/react/@algolia/autocomplete-core/@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.19.2", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/base64": ["@jsonjoy.com/base64@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/json-pack/@jsonjoy.com/json-pointer": ["@jsonjoy.com/json-pointer@17.67.0", "", { "dependencies": { "@jsonjoy.com/util": "17.67.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA=="], + + "@jsonjoy.com/fs-snapshot/@jsonjoy.com/util/@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@17.67.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q=="], + + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "cli-table3/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cli-table3/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "copy-webpack-plugin/globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], + + "css-minimizer-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "file-loader/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "file-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "hpack.js/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "hpack.js/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "hpack.js/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "html-webpack-plugin/html-minifier-terser/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "mdast-util-gfm-autolink-literal/micromark-util-character/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "null-loader/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "null-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "renderkid/htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "renderkid/htmlparser2/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "renderkid/htmlparser2/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "renderkid/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-index/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], + + "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "serve-index/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "update-notifier/boxen/camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="], + + "url-loader/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "url-loader/schema-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "url-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "webpack-dev-server/open/define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cli-table3/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "file-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "null-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "renderkid/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "url-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + } +} diff --git a/docs/docs/architecture.md b/docs/docs/architecture.md index a18a4ce..f723865 100644 --- a/docs/docs/architecture.md +++ b/docs/docs/architecture.md @@ -1,37 +1,41 @@ --- id: architecture title: Architecture -sidebar_position: 6 +sidebar_position: 7 --- # Architecture -This page explains how tile57 turns an S-57 chart cell into vector tiles, how the -codebase is layered, and the memory design that keeps it small. +This page explains how tile57 turns an S-57 chart — an ENC *cell*, in the +spec's vocabulary — into vector tiles, how the codebase is layered, and the +memory design that keeps it small. ## The pipeline -A chart cell flows through these stages, all inside the engine: +A chart flows through these stages, all inside the engine: ``` S-57 ENC cell (.000) - │ decode the binary container src/s57/iso8211.zig + │ decode the binary container src/iso8211/ (module: iso8211) ▼ S-57 feature + geometry model src/s57/ (module: s57) + │ adapt S-57 → S-101 features src/s101/ adapter.zig + ▼ +S-101 feature + attribute records │ apply S-101 portrayal src/portray/ + embedded Lua 5.4 ▼ (vendor/S-101_Portrayal-Catalogue) portrayal instruction stream - │ adapt to drawing primitives src/s100/ (module: s100) + │ parse the instruction stream src/s101/ instructions.zig ▼ scene generation src/scene/ (project + clip + draw calls) ▼ render Surface src/render/surface.zig ├─► tile surfaces: MVT / MLT encode + PMTiles src/tiles/ - │ + MapLibre style.json + portrayal assets src/assets/, src/sprite/ + │ + MapLibre style.json + portrayal assets src/style/, src/sprite/ └─► pixel surfaces: PNG raster · vector PDF · terminal text (src/render/) ``` -1. **Decode (ISO 8211).** S-57 cells use the ISO 8211 binary container format; +1. **Decode (ISO 8211).** S-57 charts use the ISO 8211 binary container format; the decoder reads its raw records and fields. 2. **Build the S-57 model.** Features (depth areas, buoys, coastlines, …), their attributes, and their geometry (assembled from the vector topology) become a @@ -45,7 +49,7 @@ render Surface src/render/surface.zig primitives: filled polygons, stroked lines, symbols, patterns, soundings, text. 5. **Generate the scene.** Each primitive is projected to web-mercator tile coordinates, clipped (extent 4096, buffer 64), and emitted as *draw calls* on a - **Surface** — the backend seam described below. + **Surface** — the backend contract described below. ### The Surface contract @@ -60,33 +64,33 @@ Unicode terminal grid (with optional ANSI colour, or real pixels inline via the kitty graphics protocol). One engine, pluggable outputs — see [The Rendering Engine](./rendering.md). -### Tile formats: MLT by default, MVT on request +### Tile formats: MLT by default, MVT optional Bakes encode **MLT** ([MapLibre Tile](https://github.com/maplibre/maplibre-tile-spec)) by default; MapLibre GL JS ≥ 5.12 decodes it natively via the vector source -`encoding` option, and the generated styles carry that hint. Pass -`--format mvt` (CLI) / `tile57_bake_opts.format` (C ABI) to bake Mapbox Vector -Tiles for consumers without an MLT decoder. Live cell-backed charts open -generating MVT for compatibility; hosts opt in to MLT with -`tile57_chart_set_tile_format`. - -### Band handoff (`smax`) - -Overlapping cells of different compilation scales are resolved per tile to the -best band. At a band's floor zoom — where a finer band hands over to the -coarser one — the coarser band's features are *carried down* into the finer -band's tiles, tagged with an `smax` denominator quantized onto the archive's -SCAMIN ladder. The style (and the pixel resolver) hides a carried copy the -moment the display gets finer than `1:smax`, so band boundaries hand off -without holes or double-draws. +`encoding` option, and the generated styles carry that hint. The engine can also +encode Mapbox Vector Tiles for consumers without an MLT decoder. +`tile57_info.tile_type` reports which encoding a chart's tiles use, so a +host hints its renderer correctly. + +### Band handoff (coverage-clipped ownership) + +Overlapping charts of different compilation scales are resolved per tile to +the best band. Rather than *carry* a coarser chart's features down into a finer +band's tiles (the old `smax`-tagged carry-down), the compositor clips each +chart to the **ownership partition**: at every tile the finer chart's M_COVR +coverage wins the ground it holds, and a coarser chart renders only where no +finer chart covers it. +Band boundaries hand off without holes or double-draws, and there is no +per-feature handoff tag for the style to gate. ### Overscale indication (`oscl`) -Per S-52 §10.1.10, every contributing cell's coverage polygon is baked as an -`OVERSC01` vertical-line hatch tagged `oscl` = the cell's compilation-scale +Per S-52 §10.1.10, every contributing chart's coverage polygon is baked as an +`OVERSC01` vertical-line hatch tagged `oscl` = the chart's compilation-scale denominator. The hatch shows only while the display is *finer* than `1:oscl`, and the style sandwiches it between the overscaled and at-scale fill passes so -finer opaque data occludes a coarser cell's hatch. The `show_overscale` +finer opaque data occludes a coarser chart's hatch. The `show_overscale` mariner toggle (default on) drives its visibility. ## The Zig modules @@ -96,14 +100,20 @@ libc/Lua) and target-agnostic: | Module | Role | |--------|------| -| `s57` | S-57 ENC cell parser + geometry model (includes the ISO/IEC 8211 decoder, `src/s57/iso8211.zig`) | -| `s100` | S-100/S-101 catalogue + portrayal adaptation | +| `iso8211` | the ISO/IEC 8211 container reader (the bottom layer; std-only) | +| `s57` | the S-57 chart parser + geometry model (reads 8211 records through `iso8211`) | +| `s101` | the S-101 catalogue, the S-57 → S-101 adapter, and the portrayal instruction stream | | `portray` | the embedded-Lua S-101 runner (links libc) | | `tiles` | MVT + MLT encoders, gzip, the PMTiles container, web-mercator tile math | | `render` | the Surface contract, the resolver (colours, display gates), and the pixel machinery (Canvas, PNG, PDF, ASCII) | | `scene` | S-57 → tile-surface scene generation + the banded ENC_ROOT baker (`bake_enc.zig`) | -| `assets` | colortables / linestyles / style / manifest generation (includes `chartstyle.zig`, the mariner-driven style builder) | -| `sprite` | S-101 sprite + area-fill pattern atlases (SVG raster; links libc) | +| `style` | the S-101 color tables and line styles, the MapLibre style.json layer set, and the S-52 `mariner` settings model (`mariner.zig`) | +| `sprite` | the S-101 sprite + area-fill pattern atlases from the catalogue Symbols/AreaFills (SVG raster; links libc) | + +The `style` and `sprite` modules generate the S-101/S-52 portrayal assets — color +tables, line styles, sprites, and patterns — from the S-101 Portrayal Catalogue. +They read the catalogue bytes as input rather than importing `s101`, so they stay +independent modules a caller can grab on their own. | `engine` | the pure packages re-exported as one import (the test root) | | `tile57` | the curated public surface (`src/tile57.zig`) | @@ -111,66 +121,75 @@ libc/Lua) and target-agnostic: imported by the pure test build. The C ABI (`src/capi.zig`) is a thin shim over the same Zig API as the `tile57` module. -## The layering: Chart / bake / style - -The public surface composes the packages into three high-level entry points: - -- **`Chart`** — open a chart from a path (`openPath`), from bytes (`openBytes`), or - as a multi-cell ENC_ROOT (`openCells` / `openCellsStreaming`), then `tile(z, x, y)`. This is the live - tile-generation path. It also reads a pre-baked **PMTiles** archive — the caller - can't tell the difference. The same handle renders finished views: - `renderView` (PNG or PDF, per its output parameter) and `renderAscii` - (`tile57_chart_render_view` / `tile57_chart_render_pdf` in the C ABI). -- **`bakeArchive`** (`scene/bake_enc.zig`) — bake a whole ENC_ROOT to one PMTiles - archive offline, band-streamed. -- **`style.build`** (`assets/chartstyle.zig`) + **`assets`** / **`sprite`** — - generate the MapLibre style and the portrayal assets it references. - -The same three are exposed across the C ABI (`tile57_chart_*`, -`tile57_bake_pmtiles` / `tile57_bake_bundle`, `tile57_build_style`, and -`tile57_bake_assets` for the portrayal assets). +## The layering: chart / compose / style + +The public surface composes the packages into high-level entry points: + +- **`Chart`** — ONE open chart, no composition. Open a baked **PMTiles** archive + (`openPmtilesPath`, mmap'd; `openBytes`) or a live chart (`openBytes` on `.000` + bytes), then take its outputs: view renders (`renderView` — PNG, PDF, or a + callback canvas; `renderSurfaceView` — world-space GPU callbacks), the cursor + pick (`queryPoint`), the metadata getters, and — for an archive — its stored + tiles verbatim through `pmtilesReader()`. In the C ABI: `tile57_chart_tile` / + `tile57_chart_png` / `tile57_chart_pdf` / `tile57_chart_canvas` / `tile57_chart_surface` / + `tile57_chart_query`. A streaming ENC_ROOT open (`openPath` / `openCharts` / + `openChartsStreaming`) is the metadata + extraction view of raw source data + (the C `tile57_enc_*` readers); it serves no tiles or renders. +- **Tile production** — bake each chart to its own PMTiles at its compilation scale + (`tile57_bake_chart_bytes`, which runs the banded bake engine `scene/bake_enc.zig` + on a single chart), then a runtime **compositor** stitches the overlapping charts + through an ownership partition and offers the SAME outputs as a chart, composed: + `tile57_compose_tile` for any `(z, x, y)` on demand, `tile57_compose_png` / + `_pdf` / `_canvas` / `_surface` / `_query` for composed views and the composed + pick. Baking is strictly per-chart: one chart, one archive. +- **`style.build`** (`style/maplibre.zig`) + **`style`** / **`sprite`** — + generate the MapLibre style and the portrayal assets it references + (`tile57_style_build` / `tile57_bake_assets` in the C ABI). ## The memory design tile57 is built to hold only its working set: -- **Lazy per-cell.** An ENC_ROOT is opened by scanning each cell's header for a - cheap spatial index (band + bbox). A cell is parsed and portrayed only when a - requested tile needs it; recently used cells are held in an LRU and evicted - under bound. The first tile over a fresh area pays a parse/portray cost (tens of - ms); after that it is cached. -- **Best-available band per tile.** Overlapping cells of different compilation - scales are resolved per tile to the best band, not all overlaid blindly. -- **Streaming open.** `openCellsStreaming` (and its on-disk driver `openPath` / - `tile57_chart_open`) take per-cell metadata (bbox + scale) plus a reader; a cell's - bytes are read only on demand and freed on eviction. A host then holds only the - working set's bytes, not the whole catalogue — the right choice for a large ENC_ROOT. -- **Band-streamed bakes.** `bakeArchive` streams band-by-band (finest → - coarsest), so peak memory tracks the largest single band rather than the whole - archive. +- **Lazy per-chart reads.** An ENC_ROOT is opened by scanning each chart's + header for a cheap spatial index (band + bbox). A chart's bytes are read and + parsed + only when a metadata or feature query needs them, then released under an LRU + bound. +- **Ownership, not overlays.** Overlapping charts of different compilation + scales are resolved by the precomputed ownership partition — each tile's + ground belongs to exactly one chart per band, so composing never loads every + overlapping chart. +- **Streaming open.** `openChartsStreaming` (and its on-disk driver `openPath`, + which backs the C `tile57_enc_*` readers) take per-chart metadata (bbox + + scale) plus a reader; a chart's bytes are read only on demand and freed on + eviction. A + host then holds only the working set's bytes, not the whole catalogue — the + right choice for a large ENC_ROOT. +- **Per-chart bakes.** Each chart bakes independently at its own compilation + scale, so a bake holds a single chart's parsed data at a time — memory + doesn't grow with the size of the catalogue. - **Tile cache.** Generated/decoded tiles are memoized per chart (keyed - `z<<48 | x<<24 | y`); `clearCache` / `tile57_chart_clear_cache` drops it to - bound long-running hosts. + `z<<48 | x<<24 | y`) and released with the chart, so a long-running host bounds + memory by closing charts it no longer renders. -## The offline chart bundle +## The live-composite bake -One `bake` command (`tile57 bake -o out/`) emits a -self-contained, relocatable directory in which the tiles and the portrayal that -renders them travel together: +One `bake` command (`tile57 bake -o out/`) writes the +live-composite structure — per-chart tiles plus the ownership partition a runtime +compositor serves them from: ``` out/ - manifest.json pins schema_version, couples the two halves - tiles/chart.pmtiles the DATA half — semantic colour tokens, palette-independent - assets/colortables.json the PORTRAYAL half — token -> hex per day/dusk/night (the only RGB) - assets/linestyles.json complex line-style metadata - assets/sprite-mln*.{json,png} the symbol + pattern atlases - assets/style-{day,dusk,night}.json the MapLibre style layers, colours pre-resolved + tiles/US5MD1MC.pmtiles one PMTiles per chart, baked at its compilation scale + tiles/US4MD81M.pmtiles (M_COVR coverage embedded in each archive's metadata) + partition.tpart the ownership partition: which chart renders which ground ``` -This works because the tiles carry S-52 colour **tokens**, never RGB. Both halves -are emitted from the *same* S-101 catalogue, so they cannot drift, and the -manifest stamps both with a `schema_version` (`tile57/1` — the -[tile-schema](./tile-schema.md) vocabulary) that a renderer checks before loading. +There is no merged archive: any `(z, x, y)` tile is composed from the overlapping +charts on demand (`tile57_compose_tile`), so re-baking one chart doesn't +rewrite the whole ENC_ROOT. The portrayal assets are generated separately (`tile57 assets` / +`style`); the tiles carry S-52 colour **tokens**, never RGB, and both halves come +from the *same* S-101 catalogue, so they cannot drift. The tile-schema vocabulary +(`tile57/2`) is the contract a renderer checks. See the [**Tile Schema**](./tile-schema.md) for the vector-tile layer contract. diff --git a/docs/docs/c-api.md b/docs/docs/c-api.md index 949191b..7b602bc 100644 --- a/docs/docs/c-api.md +++ b/docs/docs/c-api.md @@ -1,144 +1,310 @@ --- id: c-api title: C API -sidebar_position: 5 +sidebar_position: 6 --- # C API `libtile57.a` exposes the whole engine behind a thin C ABI — [`include/tile57.h`](../../include/tile57.h), prefix `tile57_`. It is a shim over -the [Zig API](./zig-api.md); the two stay in lock-step. Open a **chart**, serve -vector tiles by `(z, x, y)` — MapLibre Tiles (MLT, the default bake format) or -Mapbox Vector Tiles (MVT) — render finished PNG/PDF views, and (offline) bake -archives + bundles, build a MapLibre style, and generate portrayal assets. +the [Zig API](./zig-api.md); the two stay in lock-step. + +The pipeline is three stages, and the header (and this page) is organised the +same way: + +- **Bake** — ENC source data in, per-chart PMTiles out. Each chart bakes to + its own archive at its own compilation scale, with its M_COVR coverage + + scale embedded in the archive metadata. The bake section also carries the + raw-source readers (chart inventory, feature extraction, exchange-set + catalogue). +- **Render** — a **`tile57`** chart handle opens ONE baked archive and answers + for it with NO composition: metadata (info / SCAMIN / coverage), its stored + tiles verbatim (`tile57_chart_tile` — the primitive for writing your own + compositor), the S-52 cursor pick, and view outputs (`tile57_chart_png` / `_pdf` / + `_canvas` / `_surface`). +- **Compose** — a **`tile57_compose`** handle stitches MANY open charts through + the ownership partition and offers the SAME output set, composed: + `tile57_compose_tile` (what a live tile server hands its HTTP layer — the + bytes are MapLibre Tiles), `_png`, `_pdf`, `_canvas`, `_surface`, `_query`. + +Everything is **bake, then compose** (or bake, then render): source charts +bake once to per-chart archives; every output is produced from baked archives. + +Style + portrayal-asset generation rounds out the surface: the mariner's S-52 +display options become a concrete MapLibre style JSON plus the colortables and +sprite / pattern / glyph atlases it references. + +## Errors + +Every call that can fail returns a `tile57_status` — `TILE57_OK` (0) or a coarse +cause — and takes an optional caller-owned `tile57_error*` it fills with the +status plus a specific message on failure (a stack local is fine; nothing to +free). Results come back through out-parameters, which are always defined on +return: the result on `TILE57_OK`, `NULL`/0 otherwise. "Nothing produced" is +NOT a failure — a call that finds nothing returns `TILE57_OK` with a +`NULL`/zero out. + +```c +typedef enum { + TILE57_OK = 0, /* success */ + TILE57_ERR_BADARG, /* a NULL or out-of-range argument */ + TILE57_ERR_IO, /* a file/directory could not be opened, read, or written */ + TILE57_ERR_PARSE, /* malformed input (S-57 chart, PMTiles, partition, JSON) */ + TILE57_ERR_NOMEM, /* an allocation failed */ + TILE57_ERR_UNSUPPORTED, /* valid but unusable input */ + TILE57_ERR_RENDER, /* tile generation or rendering failed */ + TILE57_ERR_INTERNAL, /* an unexpected engine failure */ +} tile57_status; + +const char *tile57_status_str(tile57_status status); /* static strerror-style text */ + +#define TILE57_ERROR_MSG_MAX 256 +typedef struct { + tile57_status status; + char message[TILE57_ERROR_MSG_MAX]; /* NUL-terminated; "" when no detail */ +} tile57_error; +``` + +```c +tile57_chart *chart = NULL; +tile57_error err; +if (tile57_chart_open("US5MD1MC.pmtiles", &chart, &err) != TILE57_OK) { + fprintf(stderr, "open failed: %s\n", err.message); /* "path: reason" */ +} +``` :::warning Lifetime + threading -A `tile57_chart` is **not** internally synchronized — use one thread per chart. -It must also outlive every consumer still holding it: if a long-lived renderer -captures the chart, close it only once nothing can still call -`tile57_chart_tile`. `tile57_chart_tile` allocates `*out`; free it with -`tile57_free` (same length). Input bytes are copied, so the caller may free them -right after the call. +No handle is internally synchronized — use one thread per handle. Each must +also outlive every borrower still holding it: a compositor borrows its charts +(close the compositor first, then the charts), and a path-opened chart mmaps +its file, so the file must stay in place while the chart is open. Calls that +return bytes allocate `*out`; free it with `tile57_free(ptr)`. Input +bytes are copied, so the caller may free them right after the call. ::: -## Open a chart + fetch tiles +## Bake: ENC charts → per-chart archives + +Tile production is a two-step composite model. First bake each chart to its +own PMTiles at its compilation scale; the archive embeds the chart's M_COVR +coverage, compilation scale, and identity in its metadata. Then open a +**compositor** over the archives and serve any `(z, x, y)` tile on demand — +the compositor stitches the overlapping charts through an ownership partition, +handling cross-band zoom. ```c -#include "tile57.h" +/* Bake ONE chart (+ its .001.. updates, read from disk) to PMTiles bytes over its + * native band zoom range. Returned in *out/*out_len (free with tile57_free); + * NULL/0 when the chart produced no tiles. */ +tile57_status tile57_bake_chart_bytes(const char *path, uint8_t **out, size_t *out_len, + tile57_error *err); + +/* Bake `n` charts IN PARALLEL across up to `workers` threads (a MEMORY bound — + * pass a small count). out_bytes[i]/out_lens[i] receive chart i's archive or + * NULL/0; *out_baked (NULL to ignore) counts the charts that produced bytes. */ +tile57_status tile57_bake_charts(const char *const *paths, size_t n, uint32_t workers, + uint8_t **out_bytes, size_t *out_lens, + size_t *out_baked, tile57_error *err); + +/* Walk in_dir for *.000 charts and bake each IN PARALLEL to the SAME relative + * path under out_dir with a .pmtiles extension (+ an .sha sidecar). + * INCREMENTAL: a chart whose archive is already at least as new as its whole + * input (.000 + update chain) is skipped, so a re-run over an unchanged tree + * bakes nothing — *out_baked counts THIS run, and 0 over a warm cache is + * success. progress (or NULL) fires per chart, possibly from worker threads. */ +typedef void (*tile57_bake_progress)(void *ctx, uint32_t done, uint32_t total); +tile57_status tile57_bake_tree(const char *in_dir, const char *out_dir, uint32_t workers, + tile57_bake_progress progress, void *progress_ctx, + uint32_t *out_baked, tile57_error *err); + +/* Read a PMTiles archive's metadata JSON blob (decompressed); NULL/0 when the + * archive carries none. A per-chart bake embeds the chart's coverage + cscl + + * date/name under a "coverage" key. */ +tile57_status tile57_pmtiles_metadata(const uint8_t *pmtiles, size_t len, + uint8_t **out, size_t *out_len, + tile57_error *err); +``` -const char *tile57_version(void); /* "0.1.0" */ +Every baked feature carries the pick-report properties `class` (object-class +acronym), `cell` (source chart stem), and `s57` (the full S-57 attribute set as a +JSON object) — what `tile57_chart_query` and a host inspector read back. -/* Opaque chart handle. */ -typedef struct tile57_chart tile57_chart; +The `tile57 bake -o out/` CLI produces this structure +directly: `out/tiles/.pmtiles` per chart plus `out/partition.tpart`. + +### Read raw S-57 source data -/* Open an on-disk ENC_ROOT directory (or a single .000 file) as a streaming - * chart: cells are enumerated + peeked at open, then their bytes are read on - * demand (working set only). Rules are the library's embedded catalogue. - * NULL on failure. */ -tile57_chart *tile57_chart_open(const char *path); +The bake section also reads the source data directly — no handle, no bake — for +a host's import UI: -/* Open one in-memory ENC cell (base .000 bytes) as a resident chart. Bytes are - * copied. NULL on failure. */ -tile57_chart *tile57_chart_open_bytes(const uint8_t *base, size_t len); +```c +/* Per-chart metadata of the S-57 data at `path` (one .000, updates applied, or a + * whole ENC_ROOT) as a JSON array: [{"name","scale","edition","update", + * "issueDate","agency","bbox"}, ...] — a host's chart-database scan. */ +tile57_status tile57_enc_charts(const char *path, uint8_t **out, size_t *out_len, + tile57_error *err); + +/* Features for comma-separated object-class acronyms (e.g. "DEPARE,DRGARE") as + * a GeoJSON FeatureCollection: lon/lat geometry, properties = {"class", plus the + * full S-57 acronym->value attribute map}. NULL/0 when nothing matched. */ +tile57_status tile57_enc_features(const char *path, const char *classes, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* The same over in-memory base .000 bytes (from a zip member, say). */ +tile57_status tile57_enc_features_bytes(const uint8_t *base, size_t len, + const char *classes, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* Decode a CATALOG.031 exchange-set catalogue into a JSON array of its CATD + * entries — file path, longName (chart title), impl (BIN/ASC/TXT), bbox. */ +tile57_status tile57_enc_catalog(const uint8_t *catalog_031, size_t len, + uint8_t **out, size_t *out_len, tile57_error *err); +``` + +The CLI mirrors these as `tile57 cells`, `tile57 features`, and +`tile57 catalog`. + +## Render: the `tile57_chart` handle + +A `tile57_chart` is ONE baked PMTiles archive, opened for metadata and +output — with no composition (the compositor below offers the same outputs +across many charts). Open it from a path (mmap'd — a whole chart library can +be open without being resident) or from bytes (copied). + +```c +const char *tile57_version(void); /* "0.3.0" */ + +/* Opaque chart handle: one open baked archive. */ +typedef struct tile57_chart tile57_chart; -/* Open a baked PMTiles bundle from a file path. NULL on failure. */ -tile57_chart *tile57_chart_open_pmtiles(const char *path); +tile57_status tile57_chart_open(const char *path, tile57_chart **out, tile57_error *err); +tile57_status tile57_chart_open_bytes(const uint8_t *pmtiles, size_t len, + tile57_chart **out, tile57_error *err); -/* Tile encodings served by tile57_chart_tile / produced by the bakes. */ +/* Vector-tile encodings an archive can store (reported in tile57_info.tile_type; + * the engine bakes MLT). */ typedef enum { TILE57_TILE_TYPE_MVT = 1, /* Mapbox Vector Tile */ - TILE57_TILE_TYPE_MLT = 2, /* MapLibre Tile (the default bake format) */ + TILE57_TILE_TYPE_MLT = 2, /* MapLibre Tile (the bake default) */ } tile57_tile_type; /* Fixed chart metadata, for a host that frames its own camera. Bounds/anchor - * validity are flagged (false -> those fields are 0). tile_type is the encoding - * tile57_chart_tile returns: a PMTiles-backed chart reports its archive's stored - * type; a cell-backed chart reports its live generation format. */ + * validity are flagged (false -> those fields are 0). native_scale is the + * compilation scale 1:N the bake embedded (0 = unknown — derive from the zoom + * band). */ typedef struct { uint8_t min_zoom, max_zoom; uint32_t bands; /* bitmask: bit r = band rank r present */ 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 */ -} tile57_chart_info; -void tile57_chart_get_info(tile57_chart *chart, tile57_chart_info *out); + int32_t native_scale; +} tile57_info; +void tile57_chart_get_info(tile57_chart *chart, tile57_info *out); + +/* The distinct SCAMIN denominators present in the chart (ascending); NULL/0 when + * none. Free with tile57_free((uint8_t*)*out, *out_len * sizeof(int32_t)). */ +tile57_status tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len, + tile57_error *err); + +/* The chart's M_COVR data-coverage polygons, from the coverage the bake embedded: + * ring() is called once per polygon with its exterior ring as npts interleaved + * lon,lat doubles (valid only during the call). OK with no calls when the archive + * embeds none. */ +typedef struct { + void *ctx; + void (*ring)(void *ctx, const double *lonlat, size_t npts); +} tile57_coverage_cb; +tile57_status tile57_chart_coverage(tile57_chart *chart, const tile57_coverage_cb *cb, + tile57_error *err); + +/* The chart's own stored tile at (z,x,y), decompressed (MLT or MVT per + * tile57_info.tile_type), with NO composition — the per-archive primitive for + * an embedder writing its own compositor. NULL/0 when the archive has no tile + * there. */ +tile57_status tile57_chart_tile(tile57_chart *chart, uint8_t z, uint32_t x, uint32_t y, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* Release a chart and all cached tiles (not while a compositor still holds it). */ +void tile57_chart_close(tile57_chart *chart); +``` -typedef enum { - TILE57_TILE_OK = 1, /* *out / *out_len set; free with tile57_free */ - TILE57_TILE_EMPTY = 0, /* valid tile, no features */ - TILE57_TILE_ERROR = -1, -} tile57_tile_status; - -/* Fetch tile (z, x, y) as decompressed vector-tile bytes, VERBATIM in the - * chart's tile encoding (chart_info.tile_type — there is no transcode layer; - * hosts hint the encoding to the renderer instead, e.g. the MapLibre vector - * source `encoding` option). Cached per chart. */ -tile57_tile_status tile57_chart_tile(tile57_chart *chart, uint8_t z, uint32_t x, uint32_t y, - uint8_t **out, size_t *out_len); - -/* Select the encoding for LIVE-generated tiles on a cell-backed chart: 0 = - * engine default (mlt), TILE57_TILE_TYPE_MVT, TILE57_TILE_TYPE_MLT. Cell-backed - * charts OPEN generating MVT (existing MVT-only embedders are unaffected); a - * host whose renderer decodes MLT opts in here. No-op for a baked PMTiles chart. - * Changing the format drops the tile cache. */ -void tile57_chart_set_tile_format(tile57_chart *chart, uint8_t format); +### Query the features under a point (object query / pick) -/* Free ANY buffer the engine returned (tiles, style JSON, the scamin array, - * colortables, …), passing the same length. The universal free. */ -void tile57_free(void *ptr, size_t len); +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 chart name. This is what a chart application shows when you tap a +feature to see what it is. -void tile57_chart_clear_cache(tile57_chart *chart); -void tile57_chart_close(tile57_chart *chart); +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 { + void *ctx; + void (*feature)(void *ctx, const char *cls, size_t cls_len, + const char *s57, size_t s57_len, + const char *chart, size_t chart_len); +} tile57_query_cb; -/* The distinct SCAMIN denominators present in the chart (ascending). On success - * returns 1 with *out pointing at *out_len int32 values, 0 if none, -1 on error. - * Free with tile57_free((uint8_t*)*out, *out_len * sizeof(int32_t)). */ -int tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len); +/* Calls cb->feature once per displayed feature under (lon,lat) at view `zoom`. + * Callback pointers are valid only during that call. */ +tile57_status tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom, + const tile57_query_cb *cb, tile57_error *err); ``` -An ENC_ROOT cell is a base `.000` plus its sequential `.001`, `.002` … update -files; `tile57_chart_open` walks the directory (`CATALOG.031`, else a `*.000` -scan), applies each cell's updates, and overlays the cells by scale band. +The class and chart name come through for any hit; the attribute JSON is filled in +from the `s57` pick property baked into the tiles (empty if a chart was baked +without pick attributes). -## Render a finished view (PNG / PDF) +### Render a finished view (PNG / PDF), one chart The [native S-52 rendering engine](./rendering.md) draws a view of the chart — -centre + fractional zoom + pixel size — with the mariner settings evaluated -*live* (real safety contour, category/SCAMIN/text-group gates, day/dusk/night -palette), catalogue symbols replayed as vectors, and labels decluttered over the -whole canvas. +centre + fractional zoom + pixel size — by replaying the archive's baked tiles +through the S-52 pixel path: one scene across every covering tile, labels +decluttered over the whole canvas, catalogue symbols replayed as vectors. The +mariner's live-swappable settings (colour scheme, safety-contour danger and +sounding swaps, category/SCAMIN/text gates, size scale) evaluate at render +time; the rest of the portrayal context was fixed at bake time. + +`width`/`height` must be 1..16384 per side; `m` NULL = canonical defaults +(`tile57_mariner_defaults`). The `tile57_mariner` settings struct is shared +with the [style builders](#build-a-maplibre-style) below. ```c -/* PNG raster. `m` NULL = canonical defaults (tile57_mariner_defaults). Returns 0 - * with *out/*out_len set (free with tile57_free); -1 bad handle, -2 render - * failure, -3 unsupported source (a baked PMTiles chart carries no portrayal). */ -int tile57_chart_render_view(tile57_chart *chart, double lon, double lat, double zoom, - uint32_t width, uint32_t height, - const tile57_mariner *m, - uint8_t **out, size_t *out_len); +/* PNG raster in *out/*out_len (free with tile57_free). */ +tile57_status tile57_chart_png(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); /* Its vector twin: the SAME scene as a deterministic single-page PDF - * (1 px = 1 pt, 72 dpi; vector fills + glyph-outline text). Same parameters, - * returns, and ownership. */ -int tile57_chart_render_pdf(tile57_chart *chart, double lon, double lat, double zoom, - uint32_t width, uint32_t height, - const tile57_mariner *m, - uint8_t **out, size_t *out_len); + * (1 px = 1 pt, 72 dpi; vector fills + glyph-outline text). */ +tile57_status tile57_chart_pdf(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); ``` -The `tile57_mariner` settings struct is defined in the -[chart-style section](#build-a-maplibre-style) below. +The composed twins — `tile57_compose_png` / `tile57_compose_pdf`, same +parameters over a `tile57_compose` — render the same view across the WHOLE +composed set (see below). -## Render to a host surface (vector callbacks) +### 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 +You fill in a `tile57_surface_cb` vtable and pass it to `tile57_chart_surface` (or +`tile57_compose_surface` for the composed set). 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 @@ -166,13 +332,18 @@ typedef struct { 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); + /* Optional. Text as a UTF-8 string for a host SDF glyph atlas (tile57_bake_glyph_sdf), + * instead of tessellated outlines. */ + 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; -/* 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); +/* Portray the view once and drive the callbacks. */ +tile57_status tile57_chart_surface(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + const tile57_surface_cb *surface, tile57_error *err); ``` Set `draw_sprite` and `draw_pattern` once you have the sprite atlas loaded (see @@ -184,134 +355,103 @@ 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 -/* The chart's per-cell metadata as a JSON array — name (DSNM stem), scale - * (DSPM CSCL), edition/update/issueDate/agency (after the update chain), and - * bbox. 1 = *out/*out_len set (free with tile57_free); 0 = no cells (e.g. a - * PMTiles chart — its bundle manifest carries the inventory); -1 = error. */ -int tile57_chart_cells(tile57_chart *chart, uint8_t **out, size_t *out_len); - -/* The chart's features for comma-separated object-class acronyms (e.g. - * "DEPARE,DRGARE") as a GeoJSON FeatureCollection: lon/lat geometry, - * properties = {"class": …, plus the full S-57 acronym->value attribute map}. - * Parsed without portrayal; an ENC_ROOT-wide query walks every cell. 1 = JSON - * set; 0 = no matches; -1 = error. */ -int tile57_chart_features(tile57_chart *chart, const char *classes, - uint8_t **out, size_t *out_len); - -/* Decode a CATALOG.031 exchange-set catalogue (raw bytes) into a JSON array of - * its CATD entries — file path, longName (chart title), impl (BIN/ASC/TXT), - * bbox. Not chart-scoped. 1 = JSON set; 0 = no CATD records; -1 = parse error. */ -int tile57_catalog_entries(const uint8_t *catalog_031, size_t len, - uint8_t **out, size_t *out_len); -``` +There is a pixel-space twin, `tile57_chart_canvas` with a `tile57_canvas_cb` vtable, +that emits the SAME portrayal as resolved paint-order draw calls in canvas +pixels — for a host that wants the engine's own paint pipeline without the PNG +encode. Both callback forms have composed twins (`tile57_compose_canvas` / +`tile57_compose_surface`). -The CLI mirrors these as `tile57 cells`, `tile57 features`, and -`tile57 catalog`. +## Compose: many charts, one chart -## Query the features under a point (object query / pick) - -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. +The compositor builds (or loads) the ownership partition over its charts' +embedded coverage, then offers the SAME output set as a single chart, composed: +any tile on demand for the cost of a classify plus one decompress or one +decode/clip, plus the composed view outputs and the composed cursor pick. It +**borrows** the charts — their mmap'd archives and decoded coverage — so the +chart set is never fully resident and the charts must outlive the compositor. +Open once, serve many, close. ```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 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 -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 -compilation scale, so the result opens cheaply (`tile57_chart_open_pmtiles`) -instead of holding every cell live. One cell = a base `.000` plus its sequential -update files. +/* Opaque runtime-compositor handle. */ +typedef struct tile57_compose tile57_compose; -```c -/* One ENC cell for tile57_bake_pmtiles. `name` (the source cell stem, e.g. - * "US4MD81M") is emitted as the `cell` pick-report property; NULL/"" omits it. */ -typedef struct { - const uint8_t *base; size_t base_len; - const uint8_t *const *updates; const size_t *update_lens; size_t update_count; - const char *name; -} tile57_cell; - -/* Progress callback. stage 0 = loading/portraying cells, stage 1 = baking tiles. - * band_index/band_count/band_name locate the current navigational band. */ -typedef void (*tile57_bake_progress)(void *user, uint8_t stage, size_t done, size_t total, - uint8_t band_index, uint8_t band_count, - const char *band_name); - -/* Shared bake options. Pass NULL for all defaults (embedded rules/catalogue, no - * band clamp, pick attrs included, no progress). catalog_dir/created apply to - * tile57_bake_bundle only. */ +/* Coverage/zoom summary filled by tile57_compose_get_meta. */ typedef struct { - const char *rules_dir; /* NULL = embedded portrayal rules */ - const char *catalog_dir; /* NULL = embedded S-101 catalogue (bundle only) */ - const char *created; /* NULL = manifest "created" unset (bundle only) */ - uint8_t minzoom, maxzoom; /* 0/0 = no band clamp */ - bool omit_pick_attrs; - tile57_bake_progress progress; - void *progress_user; - uint8_t format; /* baked tile encoding: 0 = default (mlt), - * TILE57_TILE_TYPE_MVT, TILE57_TILE_TYPE_MLT. - * Honored by both bake calls. */ -} tile57_bake_opts; - -/* 1 with the archive in *out/*out_len (free with tile57_free), 0 if nothing - * covered, -1 on error. */ -int tile57_bake_pmtiles(const tile57_cell *cells, size_t count, - const tile57_bake_opts *opts, - uint8_t **out, size_t *out_len); + uint8_t min_zoom; + uint8_t max_zoom; /* deepest zoom served (native + one overscale zoom) */ + uint32_t charts; /* coverage-carrying charts held */ + double west, south, east, north; /* union coverage bounds, degrees */ +} tile57_compose_meta; + +/* Open a compositor over `n` open charts. Charts whose archives embed no + * coverage are skipped (they can own no ground); none at all is + * TILE57_ERR_UNSUPPORTED. partition_path (NULL to skip) names a sidecar — + * written by tile57_compose_save_partition (the `tile57 bake` CLI emits + * partition.tpart) — to load and skip the build; a missing/stale one falls back + * to building. Close with tile57_compose_close BEFORE closing the charts. */ +tile57_status tile57_compose_open(tile57_chart *const *charts, size_t n, + const char *partition_path, + tile57_compose **out, tile57_error *err); + +/* Compose tile (z,x,y) on demand into RAW (decompressed) MLT — what a live tile + * server hands its HTTP layer (which gzips on the wire). NULL/0 out with OK = + * no bytes; *out_owned (NULL to ignore) then distinguishes the two empties: + * owned=false: no chart owns this ground — true empty ocean, safe to cache; + * owned=true: a chart owns this ground but produced nothing — transient while + * its per-chart bake is running, suspect once bakes are done. */ +tile57_status tile57_compose_tile(tile57_compose *c, uint8_t z, uint32_t x, uint32_t y, + uint8_t **out, size_t *out_len, bool *out_owned, + tile57_error *err); + +/* The composed view outputs and pick — the section-4 calls across the WHOLE + * composed set: every covering tile is composed on demand (stitched + * through the ownership partition) and replayed through the S-52 pixel path. + * Same parameters, limits, and ownership as the single-chart forms. */ +tile57_status tile57_compose_png(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); +tile57_status tile57_compose_pdf(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); +tile57_status tile57_compose_canvas(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, const tile57_mariner *m, + const tile57_canvas_cb *canvas, tile57_error *err); +tile57_status tile57_compose_surface(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, const tile57_mariner *m, + const tile57_surface_cb *surface, tile57_error *err); +tile57_status tile57_compose_query(tile57_compose *c, double lon, double lat, double zoom, + const tile57_query_cb *cb, tile57_error *err); + +/* Fill *out with the compositor's zoom range + union coverage bounds. */ +void tile57_compose_get_meta(tile57_compose *c, tile57_compose_meta *out); + +/* Serialize the ownership partition to `path` (a sidecar a later + * tile57_compose_open loads to skip the build). */ +tile57_status tile57_compose_save_partition(tile57_compose *c, const char *path, + tile57_error *err); + +/* Release a compositor. Its charts stay open (and stay yours to close). */ +void tile57_compose_close(tile57_compose *c); ``` -## Bake a chart bundle - -`tile57_bake_bundle` bakes a single cell `.000` **or** a whole ENC_ROOT directory -(`input`, an on-disk path) into a self-contained chart bundle under `out_dir` — -the same package the `tile57 bake … -o out/` CLI emits: `tiles/chart.pmtiles`, -`assets/{colortables,linestyles}.json` + sprite/pattern atlases, per-scheme -`assets/style-{day,dusk,night}.json`, and `manifest.json`. `out_cell_count` / -`out_bbox` (w,s,e,n) are optional. Returns 1 on success, 0 if nothing was covered, --1 on error. - ```c -int tile57_bake_bundle(const char *input, const char *out_dir, - const tile57_bake_opts *opts, - uint32_t *out_cell_count, double *out_bbox); +/* bake -> open -> compose -> serve */ +tile57_chart *charts[2]; +tile57_chart_open("tiles/US5MD1MC.pmtiles", &charts[0], NULL); +tile57_chart_open("tiles/US5MD1MD.pmtiles", &charts[1], NULL); +tile57_compose *cmp = NULL; +tile57_compose_open(charts, 2, "partition.tpart", &cmp, NULL); +uint8_t *tile; size_t n; bool owned; +tile57_compose_tile(cmp, 13, 2359, 3139, &tile, &n, &owned, NULL); ``` ## Generate portrayal assets -`tile57_bake_assets` produces all portrayal assets in memory (the same files -`tile57_bake_bundle` writes to disk) from the library's embedded catalogue -(`catalog_dir` NULL/"") or an on-disk `PortrayalCatalog`. Every non-NULL buffer is -owned by the library; release the whole struct with `tile57_assets_free`. +`tile57_bake_assets` produces all portrayal assets in memory — colour tables, +line styles, and the sprite / area-fill pattern atlases — from the library's +embedded catalogue (`catalog_dir` NULL/"") or an on-disk `PortrayalCatalog`. +Every non-NULL buffer is owned by the library; release the whole struct with +`tile57_assets_free`. ```c typedef struct { @@ -321,28 +461,33 @@ 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); /* 1 = ok, 0 = error */ +tile57_status tile57_bake_assets(const char *catalog_dir, tile57_assets *out, + tile57_error *err); 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 +into one PNG, each atlas 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. +hand back. `tile57_bake_glyph_sdf` is its text counterpart: an RGBA +signed-distance-field atlas of the label font, for a host that draws text as SDF +quads. Free either with `tile57_assets_free` as above. ```c -int tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out); /* 1 = ok, 0 = error */ +tile57_status tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out, + tile57_error *err); +tile57_status tile57_bake_glyph_sdf(tile57_assets *out, tile57_error *err); ``` ## Build a MapLibre style -`tile57_build_style` turns a MapLibre style template + the mariner's S-52 display +`tile57_style_build` turns a MapLibre style template + the mariner's S-52 display options + the S-52 colortables into a concrete style JSON, client-side. The template + colortables come from the built-in `tile57_style_template` / -`tile57_colortables_default` (or the bundle's assets); the host fills +`tile57_colortables_default` (or the generated assets); the host fills `tile57_mariner` from its UI. ```c @@ -350,7 +495,7 @@ typedef enum { TILE57_SCHEME_DAY=0, TILE57_SCHEME_DUSK=1, TILE57_SCHEME_NIGHT=2 typedef enum { TILE57_DEPTH_METERS=0, TILE57_DEPTH_FEET=1 } tile57_depth_unit; typedef enum { TILE57_BOUNDARY_SYMBOLIZED=0, TILE57_BOUNDARY_PLAIN=1 } tile57_boundary_style; -typedef struct { +typedef struct tile57_mariner { tile57_scheme scheme; double shallow_contour, safety_contour, deep_contour, safety_depth; bool four_shade_water; @@ -375,26 +520,25 @@ typedef struct { void tile57_mariner_defaults(tile57_mariner *m); /* canonical defaults, date_view = "" */ /* enabled_bands: NULL = show all; else only features whose band rank is in the - * array. scamin: the distinct SCAMIN denominators present in the source (e.g. from - * tile57_chart_scamin) — when non-NULL the `_scamin` layers split into per-value - * native-minzoom buckets; scamin_lat is the representative latitude. Returns 1 with - * the style JSON in *out/*out_len (free with tile57_free); 0 on error. */ -int tile57_build_style(const char *template_json, size_t template_len, - const tile57_mariner *m, - const char *colortables_json, size_t colortables_len, - const int32_t *enabled_bands, size_t enabled_band_count, - const int32_t *scamin, size_t scamin_count, double scamin_lat, - uint8_t **out, size_t *out_len); + * array. scamin: the distinct SCAMIN denominators present in the source (e.g. + * from tile57_chart_scamin) — when non-NULL the `_scamin` layers split into per-value + * native-minzoom buckets; scamin_lat is the representative latitude. */ +tile57_status tile57_style_build(const char *template_json, size_t template_len, + const tile57_mariner *m, + const char *colortables_json, size_t colortables_len, + const int32_t *enabled_bands, size_t enabled_band_count, + const int32_t *scamin, size_t scamin_count, double scamin_lat, + uint8_t **out, size_t *out_len, tile57_error *err); /* Minimal MapLibre style-mutation ops to turn the style for `old_m` into the style - * for `new_m` (same inputs as tile57_build_style) — for flicker-free mariner + * for `new_m` (same inputs as tile57_style_build) — for flicker-free mariner * toggles. Writes a JSON op array to *out/*out_len (free with tile57_free). */ -int tile57_style_diff(const char *template_json, size_t template_len, - const tile57_mariner *old_m, const tile57_mariner *new_m, - const char *colortables_json, size_t colortables_len, - const int32_t *enabled_bands, size_t enabled_band_count, - const int32_t *scamin, size_t scamin_count, double scamin_lat, - uint8_t **out, size_t *out_len); +tile57_status tile57_style_diff(const char *template_json, size_t template_len, + const tile57_mariner *old_m, const tile57_mariner *new_m, + const char *colortables_json, size_t colortables_len, + const int32_t *enabled_bands, size_t enabled_band_count, + const int32_t *scamin, size_t scamin_count, double scamin_lat, + uint8_t **out, size_t *out_len, tile57_error *err); ``` The S-52 colortables and base style template are baked into the library, so a host @@ -403,21 +547,36 @@ buffer with `tile57_free`): ```c /* colortables.json (S-52 token -> hex per day/dusk/night) from the baked profile. */ -int tile57_colortables_default(uint8_t **out, size_t *out_len); +tile57_status tile57_colortables_default(uint8_t **out, size_t *out_len, + tile57_error *err); /* Base MapLibre style template (layers + chart source + sprite/glyph URLs). scheme * selects the palette; source_tiles is the {z}/{x}/{y} URL (NULL -> a default * pmtiles:// source); sprite/glyphs are base URLs (NULL omits those layers); * minzoom is the chart source's tile floor, emitted verbatim (pass the archive's * real minzoom); maxzoom 0 -> engine default. tile_encoding is the source's tile - * type (from chart_info.tile_type): TILE57_TILE_TYPE_MLT emits "encoding":"mlt" + * type (from tile57_info.tile_type): TILE57_TILE_TYPE_MLT emits "encoding":"mlt" * on the source so maplibre-gl >= 5.12 decodes MLT natively; 0 / MVT emits * nothing. */ -int tile57_style_template(tile57_scheme scheme, const char *source_tiles, - const char *sprite, const char *glyphs, - uint32_t minzoom, uint32_t maxzoom, - uint8_t tile_encoding, - uint8_t **out, size_t *out_len); +tile57_status tile57_style_template(tile57_scheme scheme, const char *source_tiles, + const char *sprite, const char *glyphs, + uint32_t minzoom, uint32_t maxzoom, + uint8_t tile_encoding, + uint8_t **out, size_t *out_len, tile57_error *err); +``` + +## Util: warmup + free + +```c +/* Populate the process-global read-only registries (feature catalogue + + * complex-linestyle table) on the calling thread. Call ONCE on your main thread + * before opening or baking charts from worker threads, so concurrent bake/render is + * race-free. Idempotent. */ +void tile57_warmup(void); + +/* Free ANY buffer the engine returned (tiles, style JSON, the scamin array, + * colortables, …) — length-prefixed, so the pointer is all it needs. */ +void tile57_free(void *ptr); ``` ## Diagnostics header @@ -428,4 +587,4 @@ part of the embedding API. ## Versioning -Pre-1.0 (`0.1.0`). No external consumers yet, so the ABI is not frozen. +Pre-1.0 (`0.3.0`). No external consumers yet, so the ABI is not frozen. diff --git a/docs/docs/cli.md b/docs/docs/cli.md new file mode 100644 index 0000000..9fe0b1c --- /dev/null +++ b/docs/docs/cli.md @@ -0,0 +1,179 @@ +--- +id: cli +title: The CLI +sidebar_position: 4 +--- + +# The CLI + +`tile57` (built to `zig-out/bin/tile57` by `zig build`) is the offline +command-line tool over the engine: it bakes charts, serves and inspects tiles, +renders finished views, and emits the portrayal assets. The S-101 Portrayal +Catalogue — rules and assets — is embedded in the binary, so no command needs +an on-disk catalogue; `--rules ` (portrayal) or a positional catalogue +path (the asset commands) overrides the embedded copy. + +Usage lines keep S-57's own word for a chart: a *cell*. `` is one +chart's base file (its sequential `.001`, `.002` … updates are discovered +automatically); `ENC_ROOT` is a whole catalogue directory. + +## Bake and serve + +### `bake` + +``` +tile57 bake -o [--rules DIR] [--from-archives] +``` + +Produces the **live-composite structure** every other output is served from: +each chart bakes at its native band scale to its own +`/tiles/.pmtiles` (M_COVR coverage embedded in the archive +metadata), and the ownership partition is written to `/partition.tpart`. +There is no merged archive — a runtime compositor serves any tile on demand — +and re-runs are incremental: an archive already newer than its whole input +(`.000` + update chain) is skipped. + +`--from-archives` treats the input as an existing directory of per-chart +archives and only (re)builds `partition.tpart` over them. + +### `compose-tile` + +``` +tile57 compose-tile [--load-partition FILE] [-o out] [--bench N] +``` + +Serves ONE composed tile on demand from a live-composite structure — the +runtime compositor as a command. `--load-partition` reuses the baked +`partition.tpart` instead of rebuilding the partition; `--bench N` times an +N×N block of tiles around `(x, y)`. + +## Render + +### `png` / `pdf` + +``` +tile57 png|pdf -o [--size N] +tile57 png|pdf --view --size WxH -o +``` + +Render one tile, or a view (any centre, fractional zoom, any pixel size), +through the [native S-52 pixel path](./rendering.md): PNG raster or +deterministic vector PDF with real text objects. An S-57 chart renders with +the S-101 rules evaluated live; a baked `.pmtiles` bundle renders by tile +replay. `--palette day|dusk|night` picks the colour scheme, `--dq` overlays +data quality, `--scale F` multiplies physical symbol size, and the mariner +settings flags (`--safety`, `--feet`, `--no-names`, …) are shown in +[The Rendering Engine](./rendering.md#from-the-command-line). + +### `ascii` + +``` +tile57 ascii --view [--size COLSxROWS] [--ansi] [--tui] [--kitty] +``` + +The chart in your terminal as a Unicode grid (default size: the terminal). +`--ansi` adds xterm-256 colour, `--tui` opens an interactive pan/zoom loop, +and `--kitty` paints real S-52 pixels inline on kitty-graphics terminals +(Ghostty, Kitty). + +## Portrayal assets and style + +Each of these takes an optional positional catalogue directory; without one it +uses the catalogue embedded in the binary. + +### `assets` + +``` +tile57 assets [portrayal-catalog-dir] -o +``` + +Emits every portrayal asset a renderer needs: `colortables.json` (S-52 token → +Day/Dusk/Night hex), `linestyles.json`, the sprite atlas (`sprite.json` + +`sprite.png`), and the area-fill pattern atlas (`patterns.json` + +`patterns.png`). + +### `style` + +``` +tile57 style [portrayal-catalog-dir] --scheme day|dusk|night -o +``` + +Emits one concrete MapLibre `style.json`, with colours resolved from the +catalogue (or `--colortables FILE`). `--source-tiles` / `--pmtiles-url` pick +the tile source, `--sprite` / `--glyphs` enable the symbol and text layers, +and `--minzoom` / `--maxzoom` bound the source. + +### `sprite` / `pattern` / `sprite-mln` + +``` +tile57 sprite|pattern|sprite-mln [portrayal-catalog-dir] -o +``` + +The focused atlas emitters: `sprite` writes the S-101 symbol atlas, `pattern` +the area-fill pattern atlas, and `sprite-mln` the MapLibre sprite sheet (every +symbol packed into one PNG, each atlas cell centered on its symbol's pivot, +plus the JSON index). + +## Inspect + +### `explore` + +``` +tile57 explore [--class ACR[,ACR..]] [--object FOID|RCID|INDEX] +``` + +The portrayal microscope: dumps, per feature, the raw S-57 (class + +attributes), the S-101 portrayal instruction stream (raw and parsed), and the +resolved Surface draw calls. Takes a single chart, or an ENC_ROOT with +`--view LON,LAT,ZOOM` (or a `…/#v=LON,LAT,ZOOM` share URL) to pull just the +charts under that viewport. `--zoom N` picks the resolving tile, `--json` +emits machine-readable output, `--no-resolve` skips the draw-call pass, and +`--tui` opens a two-pane explorer (`--kitty` adds inline render thumbnails +and a live chart map that frames the selection). + +### `cells` / `cell` / `features` / `catalog` + +``` +tile57 cells +tile57 cell +tile57 features +tile57 catalog +``` + +The raw S-57 readers (the CLI face of the `tile57_enc_*` C calls). `cells` +prints per-chart metadata for a chart or a whole catalogue — name, scale, +edition, update, issue date, agency, bbox. `cell` summarises one chart. +`features` extracts the named object classes (e.g. `DEPARE,DRGARE`) as a +GeoJSON FeatureCollection. `catalog` decodes an exchange-set catalogue into +its entries (file, title, bbox). + +### `inspect` / `tiledump` + +``` +tile57 inspect [z x y] +tile57 tiledump [--geom CLASS [--coords]] +``` + +`inspect` summarises a baked archive — zoom range and tile counts — and, given +`z x y`, one stored tile (`-o` dumps its raw decompressed bytes). `tiledump` +decodes one raw tile and summarises it: per-layer feature counts by geometry +type plus value histograms of the portrayal properties (`class`, +`symbol_name`, `ls`). `--geom CLASS` switches to per-feature geometry detail +for one class (`--coords` lists the coordinates) — for hunting degenerate +geometry. + +### `objlcount` + +``` +tile57 objlcount [prim] +``` + +Counts features of one S-57 object-class code (optionally one geometric +primitive) — a corpus-scan helper for finding charts that exercise a class. + +## `version` / `help` + +`tile57 version` prints the engine version; `tile57 help` prints the usage +summary. A few extra subcommands (`partdbg-png`, `zoomsizes`, `audit-holes`, +`audit-pairs`) are engine-development diagnostics and may change without +notice. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index e37043e..5f1f09b 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -6,25 +6,28 @@ sidebar_position: 3 # Getting Started -This guide bakes a chart with the `tile57` CLI and fetches a tile from the -engine, from both Zig and C. It assumes you have finished +This guide bakes a chart with the `tile57` CLI, then serves a composed tile and +uses the engine from Zig and C. It assumes you have finished [Installation](./installation.md) (Zig 0.16, submodules, `zig build`). ## 1. Bake a chart with the CLI -The `tile57` binary is the offline tool. It bakes ENC cells to PMTiles and emits +The `tile57` binary is the offline tool. It bakes ENC charts to PMTiles and emits the portrayal assets a renderer needs: ```sh zig build # builds zig-out/bin/tile57 T=zig-out/bin/tile57 -# One command: a single cell OR a whole ENC_ROOT (band-streamed) -> -# a self-contained bundle (tiles/chart.pmtiles + style + assets + manifest) +# Bake a single chart OR a whole ENC_ROOT to the live-composite structure: +# per-chart tiles/.pmtiles (native band scale, M_COVR embedded) + partition.tpart $T bake CELL.000 -o out/ $T bake /path/to/ENC_ROOT -o out/ -# Just the portrayal assets (from the embedded catalogue — no path needed) +# Serve one composed tile on demand from that structure (the runtime compositor) +$T compose-tile out/tiles 15 9371 12534 --load-partition out/partition.tpart -o tile.mlt + +# The portrayal assets a renderer needs (from the embedded catalogue — no path needed) $T assets -o assets/ # colortables + linestyles + sprite + patterns $T sprite-mln -o assets/ # the MapLibre sprite sheet # (pass a /path/to/PortrayalCatalog to use an on-disk catalogue instead) @@ -34,23 +37,22 @@ $T png CELL.000 --view -76.48,38.974,15 --size 1600x1200 -o chart.png $T pdf CELL.000 --view -76.48,38.974,15 --size 1600x1200 -o chart.pdf # Inspect / summarise -$T inspect out/tiles/chart.pmtiles # zoom range + tile counts -$T cell CELL.000 # summarise an S-57 cell +$T inspect out/tiles/US5MD1MC.pmtiles # zoom range + tile counts for one chart +$T cell CELL.000 # summarise an S-57 chart $T version ``` -Run `tile57 help` for usage. The full subcommand list: `bake`, `assets`, -`sprite`, `pattern`, `sprite-mln`, `style`, `png`, `pdf`, `ascii`, `cells`, -`catalog`, `features`, `inspect`, `cell`, `objlcount`, `version`, `help`. +Run `tile57 help` for usage, or see [**The CLI**](./cli.md) for the full +command reference. :::info Tiles are MLT by default Bakes encode [MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec) (MLT) by default; rendering them needs **MapLibre GL JS ≥ 5.12** (which decodes -MLT natively — the generated styles carry the source `encoding` hint). Pass -`--format mvt` to bake Mapbox Vector Tiles for older or other consumers. +MLT natively — the generated styles carry the source `encoding` hint). The +engine can also encode Mapbox Vector Tiles for consumers without an MLT decoder. ::: -And a crowd-pleaser — the chart in your terminal, as a Unicode grid with +The chart also renders in your terminal, as a Unicode grid with ANSI colour, with `--tui` for an interactive pan/zoom loop (`--kitty` paints real S-52 pixels inline on kitty-graphics terminals like Ghostty or Kitty): @@ -58,73 +60,88 @@ real S-52 pixels inline on kitty-graphics terminals like Ghostty or Kitty): $T ascii CELL.000 --view -76.48,38.974,13 --ansi --tui ``` -A **bundle** is a relocatable directory in which the tiles and the portrayal that -renders them travel together: +`tile57 bake` writes a **live-composite structure** — per-chart tiles plus an +ownership partition — that a runtime compositor serves tiles from on demand: ``` out/ - manifest.json pins schema_version, couples the two halves - tiles/chart.pmtiles the DATA half — S-52 colour tokens, palette-independent - assets/colortables.json token -> hex per day/dusk/night (the only RGB) - assets/style-{day,dusk,night}.json MapLibre style layers, colours pre-resolved + tiles/US5MD1MC.pmtiles one PMTiles per chart, baked at its compilation scale + tiles/US4MD81M.pmtiles (M_COVR coverage embedded in each archive's metadata) + partition.tpart the ownership partition: which chart renders which ground ``` -## 2. Fetch a tile from Zig +There is no merged archive: any `(z, x, y)` tile is composed from the overlapping +charts on demand, so a re-bake of one chart doesn't rewrite the whole +ENC_ROOT. The +portrayal assets travel separately (`tile57 assets` / `style`); the tiles carry +S-52 colour **tokens**, never RGB, so one set of tiles renders in any palette. -Add tile57 as a dependency and `@import("tile57")`: +## 2. Serve a tile from C -```zig -const tile57 = @import("tile57"); +The engine sits behind a thin C ABI ([`include/tile57.h`](../../include/tile57.h)). +Open a compositor over the bake output and serve tiles by `(z, x, y)`: -// Open a chart: a PMTiles archive, or a raw S-57 cell portrayed live. -var chart = try tile57.Chart.openBytes(cell_bytes, .auto, null); -defer chart.deinit(); +```c +#include "tile57.h" -if (try chart.tile(z, x, y)) |bytes| { // decompressed tile bytes (or null if empty) - defer tile57.freeBytes(bytes); - // … hand the tile to your renderer … +/* Open each baked archive as a chart, then a compositor over the charts. */ +tile57_chart *chart = NULL; +tile57_error err; +if (tile57_chart_open("out/tiles/US5MD1MC.pmtiles", &chart, &err) != TILE57_OK) { + fprintf(stderr, "%s\n", err.message); } +tile57_compose *src = NULL; +tile57_compose_open(&chart, 1, "out/partition.tpart", &src, &err); + +uint8_t *tile; size_t n; bool owned; +if (tile57_compose_tile(src, z, x, y, &tile, &n, &owned, &err) == TILE57_OK) { + if (tile) { /* … serve the decompressed MLT tile … */ tile57_free(tile); } + else if (owned) { /* owned but empty — a chart's bake is still in flight */ } + else { /* not owned — open ocean; cache as blank */ } +} +tile57_compose_close(src); /* the compositor borrows its charts… */ +tile57_chart_close(chart); /* …so close them after it */ ``` -Tiles come back in the chart's tile encoding — a baked PMTiles archive serves -its stored format (MLT by default), a live cell-backed chart opens generating -MVT and opts in to MLT via `setTileFormat`; `tileType()` tells you which. - -`Chart.openPath` / `Chart.openCellsStreaming` open a whole ENC_ROOT; -`tile57.bakeArchive` bakes one to a PMTiles archive; `tile57.assets`, -`tile57.sprite`, and `tile57.style.build` generate the style + assets. See the -[Zig API](./zig-api.md). +Link against `libtile57.a`. The `partition.tpart` sidecar (NULL to skip) lets the +compositor load the ownership partition instead of rebuilding it. The chart and +the compositor offer the same outputs — `tile57_chart_png(chart, …)` renders one chart +alone; `tile57_compose_png(src, …)` renders the composed set; `tile57_chart_tile` +hands back one chart's stored tiles verbatim for anyone writing their own +compositor. Raw S-57 reading (chart inventory, feature extraction) is +handle-free via `tile57_enc_*`. See the [C API](./c-api.md). -## 3. Fetch a tile from C +## 3. Use the engine from Zig -The same engine sits behind a thin C ABI ([`include/tile57.h`](../../include/tile57.h)): - -```c -#include "tile57.h" +Add tile57 as a dependency and `@import("tile57")`: -tile57_chart *c = tile57_chart_open_bytes(data, len); /* one in-memory S-57 cell */ +```zig +const tile57 = @import("tile57"); -uint8_t *tile; size_t n; -if (tile57_chart_tile(c, z, x, y, &tile, &n) == TILE57_TILE_OK) { - /* … render the decompressed tile bytes (MVT or MLT — - see tile57_chart_info.tile_type) … */ - tile57_free(tile, n); -} -tile57_chart_close(c); +// Open one baked archive as a chart (mmap'd): metadata, pick, view renders. +var chart = try tile57.Chart.openPmtilesPath(io, "out/tiles/US5MD1MC.pmtiles"); +defer chart.deinit(); +const bbox = chart.bounds(); // geographic extent [w, s, e, n], or null + +// Or compose the whole bake output and take any output from it. +var src = (try tile57.compose.ComposeSource.openFiles(io, gpa, paths, "out/partition.tpart")).?; +defer src.deinit(); +const result = try src.tile(gpa, 15, 9371, 12534); // one composed tile +const png = try tile57.compose.renderView(src, -76.48, 38.974, 13.5, + 1600, 1200, .day, &settings, .png, null); // one composed view ``` -Link against `libtile57.a`. Three explicit openers cover the inputs: -`tile57_chart_open_bytes` (one in-memory S-57 cell), `tile57_chart_open` (an -on-disk ENC_ROOT directory or a single `.000`, streamed), and -`tile57_chart_open_pmtiles` (a baked archive). See the [C API](./c-api.md) for the -bake, style, and asset-generation entry points. +Baking (`tile57.bake.tree`), raw S-57 reading (`Chart.openPath` + +`chartsJson`/`featuresJson`), and the style builders are all in the same +package. See the [Zig API](./zig-api.md). ## ENC_ROOT and updates -Open an ENC_ROOT (many cells, each with its sequential `.001`, `.002` … updates) -with `Chart.openPath` / `tile57_chart_open` — a single call that streams a whole -on-disk catalogue, reading each cell's bytes on demand. The engine parses, applies -the updates, and serves the best-available scale band per tile. +Open an ENC_ROOT (many charts, each with its sequential `.001`, `.002` … updates) +with `Chart.openPath` (Zig) or scan it with `tile57_enc_charts` (C) — a single +call that walks a whole on-disk catalogue, applying each chart's updates. Baking +(`tile57 bake` / `tile57_bake_tree`) turns it into the per-chart archives the +compositor serves. See the [**Architecture**](./architecture.md) page for how the engine fits together. diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 07f204e..02b8c6d 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -41,10 +41,10 @@ zig build test # runs the test suite | Target | What it is | |--------|-----------| -| `tile57` (`zig-out/bin/tile57`) | the offline CLI: bake cells/ENC_ROOTs to PMTiles or a chart bundle, and emit portrayal assets. | +| `tile57` (`zig-out/bin/tile57`) | the offline CLI: bake charts/ENC_ROOTs to PMTiles or a chart bundle, and emit portrayal assets. | | `libtile57.a` | the static library behind the [C ABI](./c-api.md) (`include/tile57.h`). | -The engine is also a Zig package named `tile57` (v0.1.0); a Zig consumer +The engine is also a Zig package named `tile57` (v0.3.0); a Zig consumer depends on it and uses `@import("tile57")` — see the [Zig API](./zig-api.md). :::note Consume it as a path dependency (for now) @@ -66,7 +66,7 @@ build. Until that's fixed, use a path dependency. ## Runtime knob -- `TILE57_S101_RULES=` — S-101 portrayal rules directory for raw S-57 cells. +- `TILE57_S101_RULES=` — S-101 portrayal rules directory for raw S-57 charts. An **override**: the rules are embedded in the binary by default, so this is only needed to portray against a different on-disk catalogue (it applies when a caller passes `NULL`/`null` for the `rules_dir` argument). The CLI accepts the diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 45d2cdc..915cb2c 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -10,90 +10,98 @@ sidebar_position: 1 :::warning Not for navigation This project is coded almost entirely with AI (Claude) and human-reviewed. It is -an experiment in building a large, complex specification (IHO S-101) with AI — -not a certified or tested navigation product. **Do not rely on it for real-world -navigation.** See [Known limitations](./limitations.md). +an experiment in using AI to implement a large, complex specification (IHO +S-101) — not a certified or tested navigation product. **Do not rely on it for +real-world navigation.** See [Known limitations](./limitations.md). ::: -**tile57** is a high-performance, low-memory **S-57/S-101 → vector-tile + -S-52 style engine**, embeddable from **Zig**, **C**, or **Go**, targeting native -and WASM. It decodes IHO/NOAA **S-57** ENC cells and generates **vector tiles** -by `(z, x, y)` — [MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec) -(MLT, the default) or Mapbox Vector Tiles (MVT) — running the official IHO -**S-101 Portrayal Catalogue** in embedded Lua to produce S-52 nautical -portrayal. Alongside the tiles it emits a **MapLibre GL style** and the -portrayal **assets** it references — colour tables, line styles, and the sprite -+ area-fill pattern atlases — so a renderer such as -[MapLibre](https://github.com/maplibre/maplibre-native) can draw a chart directly -from tile57's output. +**tile57** is a nautical chart engine. It reads IHO **S-57** ENC cells — each +cell is one electronic navigational chart, compiled for one scale; NOAA and +other hydrographic offices publish thousands — converts each chart's features +to the newer **S-101** data model, and runs the official IHO **S-101 +Portrayal Catalogue** in embedded Lua to decide how each feature is drawn, +producing **S-101 symbology** (the successor to S-52). The conversion is an +interim step, and a best-effort one — S-57 has no perfect S-101 translation +(see [Known limitations](./limitations.md)); the goal is S-101 throughout, +reading native S-101 charts directly as hydrographic offices publish them. + +The primary output is **vector tiles**, fetched by `(z, x, y)`: +[MapLibre Tiles](https://github.com/maplibre/maplibre-tile-spec) (MLT, the +default) or Mapbox Vector Tiles (MVT). Alongside the tiles, tile57 emits the +matching **MapLibre GL style** and the portrayal **assets** it references — +colour tables, line styles, and the sprite and area-fill pattern atlases — so +a renderer such as [MapLibre](https://github.com/maplibre/maplibre-native) +draws a complete chart from tile57's output alone. tile57 also contains a [native S-52 rendering engine](./rendering.md): the same scene that becomes tiles can be drawn straight to **PNG**, deterministic vector **PDF**, or a **terminal** (Unicode grid / kitty graphics), with the mariner's display settings evaluated live — no browser or GPU involved. -## Goals +The whole engine is one **Zig** library behind a **C ABI**, with **Go** +bindings in the repo; it compiles natively (Linux, macOS, Windows) or to +**WASM**. -tile57 is an experiment in building a real, spec-faithful nautical chart engine -almost entirely with AI assistance. A few specific goals shape its design: +:::note Goals -- **AI-written, human-reviewed.** Every significant piece of this codebase was - generated by Claude and reviewed by a human. The project tests how far AI can - carry the heavy lifting of spec interpretation, test coverage, and correctness - on a non-trivial domain. +tile57 is an experiment in building a real, spec-faithful nautical chart +engine almost entirely with AI assistance: -- **Spec adherence first.** The goal is to implement S-57 decoding, S-101 - portrayal, and S-52 display as faithfully as possible, using the actual IHO - spec documents and the official Portrayal Catalogue — not approximations. +- **AI-written, human-reviewed** — every significant piece was generated by + Claude and reviewed by a human. +- **Spec adherence first** — the actual IHO documents and the official + Portrayal Catalogue, not approximations. +- **Cross-platform via Zig** — one core compiles to native (Linux, macOS, + Windows) and WASM without code changes. +- **Coupled tile + style** — tiles and the MapLibre style that draws them ship + together, so native and web renderers share one chart look. +- **Language-agnostic embedding** — a thin C ABI bridges the Zig core to any + language with C FFI; Go bindings ship in the repo. -- **Cross-platform via Zig.** Zig's build system and cross-compilation support let - the same core compile to native (Linux, macOS, Windows) and **WASM** without - code changes. Go and Zig were chosen specifically because both have excellent - build systems and first-class WASM targets, making the engine usable in desktop - apps, servers, and browsers from one codebase. - -- **Coupled tile + style.** The engine emits vector tiles (MLT or MVT) *and* a - matching MapLibre GL style together. The same style works for MapLibre Native - and MapLibre GL JS, so native and web renderers share one chart look without - separate style maintenance. - -- **Language-agnostic embedding.** A thin C ABI (`libtile57.a`) bridges the Zig - core to any language with C FFI. Go bindings ship in the repo; others are - straightforward additions. +::: ## The pipeline ``` S-57 ENC cell (.000) - │ ISO 8211 decode src/s57/iso8211.zig + │ ISO 8211 decode src/iso8211/ ▼ S-57 feature + geometry model src/s57/ - │ S-101 portrayal (embedded Lua) src/portray/ + src/s100/ + │ adapt S-57 → S-101 features src/s101/ (adapter) ▼ -portrayal instruction stream +S-101 feature records + │ S-101 portrayal (embedded Lua) src/portray/ + rules + ▼ +portrayal instruction stream src/s101/ (instructions) │ scene generation src/scene/ (project + clip + draw calls) ▼ render Surface ──► MVT / MLT tiles + PMTiles (src/tiles/) + MapLibre style.json + assets └───► PNG raster / vector PDF / terminal text (src/render/) ``` -## Why tile57 is fast and small - -The engine is **high-performance and low-memory by design**: - -- **Lazy, per-cell work.** A multi-cell ENC_ROOT is indexed cheaply (band + - bbox); cells are parsed and portrayed only when a requested tile needs them, - then held under an LRU bound. A **streaming** open reads a cell's bytes on - demand (and frees them on eviction), so a host holds only the working set — not - the whole catalogue. -- **Band-streamed bakes.** Baking an ENC_ROOT to one PMTiles archive streams - band-by-band (finest → coarsest, best-band dedup), so peak memory tracks the - largest single band. -- **Pure-Zig core.** The foundational format/encode modules (`s57` — including - its ISO 8211 decoder — `s100`, `tiles`, `render`, `scene`, `assets`) have no - libc; only the Lua portrayal (`portray`) and the sprite rasterizer (`sprite`) - pull in C. +This is the flow for one chart. The organizing principle is **bake, then +compose**: each source chart bakes once to its own PMTiles archive, and every +output — a served tile, a PNG, a query — is produced from baked archives, with +overlapping charts stitched into one on demand by the +[runtime compositor](./architecture.md). + +## The memory model + +The engine holds only its working set: + +- **Lazy, per-chart reads.** A multi-chart ENC_ROOT is indexed cheaply (band + + bbox); a **streaming** open reads a chart's bytes only when a metadata or + feature query needs them (and frees them on eviction), so a host holds only + the working set — not the whole catalogue. +- **Per-chart bakes, composed on demand.** Each chart bakes to its own + PMTiles at its compilation scale, so a bake holds one chart at a time; the + runtime compositor mmaps the archives and stitches the charts by `(z, x, y)` + on demand through an ownership partition, so serving never loads the chart + set either. +- **Pure-Zig core.** The foundational format/encode modules (`iso8211`, `s57`, + `s101`, `tiles`, `render`, `scene`, `style`) have no libc; only the Lua + portrayal (`portray`) and the sprite rasterizer (`sprite`) pull in C. ## Portrayal @@ -105,8 +113,9 @@ hex, so a renderer can switch palette without regenerating tiles. ## Where to go next - [**Installation**](./installation.md) — Zig 0.16, submodules, `zig build`. -- [**Getting Started**](./getting-started.md) — bake a bundle and fetch a tile +- [**Getting Started**](./getting-started.md) — bake charts and fetch a tile from Zig or C. +- [**The CLI**](./cli.md) — `tile57` bake / render / inspect commands. - [**Zig API**](./zig-api.md) — the `@import("tile57")` surface. - [**C API**](./c-api.md) — the `tile57_*` C ABI (`include/tile57.h`). - [**Architecture**](./architecture.md) — the pipeline and the Zig packages. diff --git a/docs/docs/limitations.md b/docs/docs/limitations.md index b4511ba..600b660 100644 --- a/docs/docs/limitations.md +++ b/docs/docs/limitations.md @@ -1,13 +1,13 @@ --- id: limitations title: Known Limitations -sidebar_position: 8 +sidebar_position: 9 --- # Known Limitations tile57 runs the official IHO S-101 Portrayal Catalogue, and on the -test cells (`US4MD81M.000`, `US5MD1MC.000` + updates) **every feature portrays +test charts (`US4MD81M.000`, `US5MD1MC.000` + updates) **every feature portrays with zero rule errors** — depth areas and contours, soundings, coastline and land, buoys and beacons, lights (including sector legs and arcs), dangers (obstructions / wrecks / rocks), data-quality zones, restricted/anchorage areas, @@ -22,10 +22,27 @@ See the warning on the [introduction](./intro.md). NOAA ENC charts are U.S. publ domain and not for navigation; this renderer adds its own gaps on top. ::: +## S-57 → S-101 conversion + +tile57's input is S-57 but its portrayal rules are S-101, so every chart passes +through an S-57 → S-101 adapter (`src/s101/adapter.zig`) before the rules run. +The adapter is an interim solution — the goal is S-101 throughout, reading +native S-101 charts directly as hydrographic offices publish them. S-57 has no +perfect S-101 translation; the adapter follows the IHO S-65 conversion +guidance, and the result is **best effort**: + +- **Missing S-101 content stays missing.** S-101 attributes and features with + no S-57 source are never invented; rules that test them take their fallback + branch. +- **Unconvertible objects fall back or drop.** An S-57 object class with no + S-101 equivalent portrays as the S-52 question mark (QUESMRK1) — or, where + the S-65 guidance says the object is simply not carried into S-101 (e.g. an + area-geometry recommended track), it is dropped. + ## Portrayal gaps - **Ignored portrayal-instruction keys.** The instruction translator - (`src/s100/s101_instr.zig`) lowers the drawing vocabulary the catalogue + (`src/s101/instructions.zig`) lowers the drawing vocabulary the catalogue actually leans on (point/line/text instructions, colour fills, area-fill and viewing-group references, `AugmentedRay` / `ArcByRadius` / `AugmentedPoint` construction — that is how light-sector legs and arcs render). A few keys are @@ -37,12 +54,16 @@ domain and not for navigation; this renderer adds its own gaps on top. S-52 display length; the mariner's *full-length sector line* variant (legs drawn to the light's nominal range) is not emitted, so the `show_full_sector_lines` setting currently has nothing to act on. -- **Sector figures stop at the cell's tile span.** Legs and arcs are - tessellated per tile, and only tiles covering the source cell's bounding box - are generated for that cell — a fixed-display-size arc near a cell edge is - chopped where the neighbouring tile isn't baked from that cell. +- **Sector figures can stop at a tile boundary beyond the owning chart.** The + compositor keeps a light's sector legs and arcs WHOLE across ownership + boundaries (they are fixed-size decorations anchored at the light, exempt + from face clipping). The remaining gap: a figure reaching into a tile where the owning + chart holds no ground at all is absent there — the compositor never consults + that chart for the tile — so a figure within roughly one tile of the chart's + owned ground can cut at that tile's edge (directional ground-length legs can + reach further). - **Single-primitive rules vs. non-conformant geometry.** Some S-101 rules - handle only one primitive (e.g. RecommendedTrack is Curve-only); a cell that + handle only one primitive (e.g. RecommendedTrack is Curve-only); a chart that encodes the feature with another primitive (an area-encoded recommended track) errors in the rule and the feature is suppressed. - **Low-accuracy sounding ring via spatial QUAPOS.** SNDFRM04's low-accuracy @@ -61,7 +82,7 @@ domain and not for navigation; this renderer adds its own gaps on top. indication (`OVERSC01`, see [architecture](./architecture.md)) is gated correctly everywhere, but only the generated MapLibre style sandwiches the hatch under finer at-scale fills. The PNG/PDF/ASCII surfaces draw in S-52 - priority order without that sandwich, so where a finer cell overlaps a + priority order without that sandwich, so where a finer chart overlaps a coarser one the hatch can show through fills that should occlude it. - **SCAMIN gating snaps to integer zooms outside bucket mode.** With a SCAMIN manifest, the style builds per-value layers with exact fractional native @@ -77,19 +98,18 @@ has its own short list of deliberate gaps — see ## ENC_ROOT loading -Opening an ENC_ROOT (`Chart.openPath` / `openCellsStreaming`) builds a cheap -spatial index (band + bbox per cell) and generates tiles **on demand**, parsing + -portraying only the cells a requested tile needs (best-available scale band per -tile, recent cells held in an LRU). The catalogue opens in seconds; memory stays -bounded. Caveats: +Opening an ENC_ROOT (`Chart.openPath` / `openChartsStreaming`) builds a cheap +spatial index (band + bbox per chart) and reads a chart's bytes only when a +metadata or feature query needs them — the catalogue opens in seconds and +memory stays bounded. It serves metadata and extraction only: tiles and views +always come from a bake (bake each chart once, then compose on demand). +Caveats: -- **First-view latency.** The first tile over a fresh area parses + portrays its - 1–4 cells (tens of ms), then they're cached. Opening the whole NOAA catalogue - also pays a one-time index scan (a few seconds, parsing every cell header). -- **Offline bake.** For smooth panning everywhere, bake the ENC_ROOT once to a - cached PMTiles archive (`tile57 bake`, or `bakeArchive` / - `tile57_bake_pmtiles`) and open that instead. The whole catalogue is a - multi-minute one-time bake; a region is far quicker. +- **Baking is the import step.** The whole NOAA catalogue is a multi-minute + one-time bake (`tile57 bake` / `tile57_bake_tree`); a region is far quicker, + and re-runs are incremental — only charts whose source changed re-bake. +- **Index-scan cost.** Opening the whole catalogue as a streaming chart pays a + one-time index scan (a few seconds, parsing every chart header). - **Low zoom is style-gated.** The generated style's vector-source `minzoom` is the bake's tile floor (default 8), and MapLibre never requests tiles below a source's minzoom — nothing draws below it regardless of the data. diff --git a/docs/docs/rendering.md b/docs/docs/rendering.md index d8ee86c..b2a95e9 100644 --- a/docs/docs/rendering.md +++ b/docs/docs/rendering.md @@ -1,13 +1,13 @@ # The Rendering Engine tile57 contains a full **native S-52 rendering engine**: it draws a finished -chart — raster PNG or vector PDF — straight from ENC cells, with no MapLibre, +chart — raster PNG or vector PDF — straight from ENC charts, with no MapLibre, browser, or GPU involved. This page explains how it works, how to use it, and how to extend it. ## The one-paragraph version -Charts are turned into **draw calls**. The *scene generator* reads cells, runs +Charts are turned into **draw calls**. The *scene generator* reads charts, runs the official S-101 portrayal rules, and calls methods like `fillArea("DEPVS", rings)` or `drawSymbol("BOYLAT13", point)` on a **Surface**. What happens next depends on which Surface is listening: the tile surface *serializes* those @@ -20,7 +20,7 @@ print. One engine, pluggable outputs. ``` ┌──────────────────────────────────────────────┐ - ENC cells ─►│ scene generator (src/scene/) │ + ENC chart ─►│ scene generator (src/scene/) │ │ parse → S-101 portrayal (embedded Lua) → │ │ project → clip → draw calls, per tile/view │ └───────────────────┬──────────────────────────┘ @@ -85,27 +85,31 @@ byte-deterministic: the same scene renders the same file, every time. The tile path has to **freeze** the portrayal context at bake time (a tile archive can't re-run Lua per user), and papers over it with swappable -properties the style toggles at runtime. The native path has no such limit: -`render_view` runs the S-101 rules with the mariner's *actual* safety -contour, boundary style, and point-symbol style. What you see is what the -rules decided for *your* settings — the ECDIS-faithful path. +properties the style toggles at runtime. The pixel path evaluates the +mariner's display gates — palette, display category, SCAMIN, viewing groups, +text groups, size scale — live at render time for any source. Rendering a +live chart (the CLI on a `.000`, or `Chart.openBytes` in Zig) goes further: +the S-101 rules themselves run with the mariner's *actual* safety contour, +boundary style, and point-symbol style — what you see is what the rules +decided for *your* settings, the ECDIS-faithful path. Rendering a baked +archive replays its tiles, so the rule outcomes are the bake's; the +swappable parts re-evaluate. ## Using it ### From the command line ```sh -# One tile of a cell, as a 512px PNG +# One tile of a chart, as a 512px PNG tile57 png US5MD1MC.000 14 4712 6280 -o tile.png --size 512 -# A view (any centre, fractional zoom, any size) from a whole ENC_ROOT — -# cells are selected and quilted per band automatically -tile57 png ~/charts/ENC_ROOT --view -76.48,38.974,15.1 --size 1600x1200 -o annapolis.png +# A view (any centre, fractional zoom, any size) from a single chart +tile57 png US5MD1MC.000 --view -76.48,38.974,15.1 --size 1600x1200 -o annapolis.png # The same view as a vector PDF with selectable text -tile57 pdf ~/charts/ENC_ROOT --view -76.48,38.974,15.1 --size 1600x1200 -o annapolis.pdf +tile57 pdf US5MD1MC.000 --view -76.48,38.974,15.1 --size 1600x1200 -o annapolis.pdf -# From a baked PMTiles bundle instead of source cells (tile replay) +# From a baked PMTiles bundle instead of the source chart (tile replay) tile57 png chart.pmtiles --view -76.48,38.974,15.1 --size 1024x768 -o out.png # Mariner settings @@ -115,10 +119,13 @@ tile57 png ... --safety 5 --safety-depth 5 --feet --palette night \ ### From C (and therefore Go, Python, C++, …) -Two calls in `include/tile57.h`, mirroring `tile57_chart_tile`: +Everything is bake, then render: open a baked archive as a chart and ask it +for a view (same allocate-`*out` / free-with-`tile57_free` convention as the +rest of the ABI): ```c -tile57_chart *c = tile57_chart_open("/path/to/ENC_ROOT"); +tile57_chart *c = NULL; +tile57_chart_open("US5MD1MC.pmtiles", &c, NULL); /* a baked archive */ tile57_mariner m; tile57_mariner_defaults(&m); @@ -126,14 +133,19 @@ m.safety_contour = 5.0; m.scheme = TILE57_SCHEME_NIGHT; uint8_t *png; size_t len; -tile57_chart_render_view(c, -76.48, 38.974, 15.1, 1600, 1200, &m, &png, &len); +tile57_chart_png(c, -76.48, 38.974, 15.1, 1600, 1200, &m, &png, &len, NULL); /* ... write/display png ... */ -tile57_free(png, len); +tile57_free(png); uint8_t *pdf; size_t plen; -tile57_chart_render_pdf(c, -76.48, 38.974, 15.1, 1600, 1200, &m, &pdf, &plen); +tile57_chart_pdf(c, -76.48, 38.974, 15.1, 1600, 1200, &m, &pdf, &plen, NULL); ``` +That renders ONE chart, no composition. A view across a whole chart library +is the same call on the compositor — `tile57_compose_png` / `_pdf` — which +composes every covering tile through the ownership partition first. See the +[C API](./c-api.md). + `m.size_scale` calibrates physical size (so 1 S-52 millimetre is a true millimetre on your display) and doubles as the @2x knob. Every field of `tile57_mariner` — categories, text groups, contours, units — evaluates live. @@ -158,18 +170,21 @@ the ten Surface methods and you receive the full semantic stream — this is how MVT and MLT are done (`TileSurface` in `src/scene/scene.zig`), and how a GeoJSON debug dump or a GPU display list would be done. -**From the C ABI:** not currently supported. Surface and Canvas are Zig -interfaces; the C ABI exposes the products (tiles, PNG, PDF, styles, assets) -and the inputs (charts, mariner settings), not the interfaces themselves. A -C-callback Canvas — C function pointers receiving resolved paths and glyph -runs — would be a mechanical addition and may come later. Until then, a -custom output format means a small Zig file in `src/render/`, wired into the -build in one place. +**From the C ABI:** both interfaces are exposed as callback tables. +`tile57_chart_canvas` drives a `tile57_canvas_cb` — C function pointers receiving +resolved, flattened paths, patterns, and glyph outlines in pixel space, in +paint order (the Canvas seat). `tile57_chart_surface` drives a `tile57_surface_cb` — +the world-space, semantically tagged stream (per-feature class + SCAMIN, world +anchors, reference-pixel outlines) a GPU host tessellates once and transforms +per frame (the Surface seat). Both have composed twins on the compositor +(`tile57_compose_canvas` / `tile57_compose_surface`). A custom output format in +Zig is still one small file in `src/render/`. ## What's deliberately not here (yet) - Contour labels render horizontal (not rotated along the line). - No kerning (chart labels are short; Noto's advances read fine). - Translucent fills print opaque in PDF. -- PMTiles replay doesn't overzoom past the archive's baked range. +- Archive replay doesn't overzoom past the baked range (the compositor serves + one fill-up zoom past it). - The PDF embeds the whole label font (~600 KB) rather than a subset. diff --git a/docs/docs/tile-schema.md b/docs/docs/tile-schema.md index 2fd73f3..a9ea7e0 100644 --- a/docs/docs/tile-schema.md +++ b/docs/docs/tile-schema.md @@ -1,7 +1,7 @@ --- id: tile-schema title: Tile Schema -sidebar_position: 7 +sidebar_position: 8 --- # Tile Schema @@ -9,22 +9,21 @@ sidebar_position: 7 tile57's vector tiles use a fixed set of layers and fields. The generated MapLibre style depends on this schema, so the names are a contract. Do not rename a layer or a field without updating the style generator -(`src/assets/style.zig`) to match **and bumping the schema version**. +(`src/style/maplibre.zig`) to match **and bumping the schema version**. -This vocabulary is versioned as **`tile57/1`**. A -[chart bundle](./architecture.md#the-offline-chart-bundle) stamps both halves — -the tiles and the portrayal assets — with that `schema_version` in its -`manifest.json`, so a renderer can refuse a bundle whose schema it doesn't speak. -Any change to a layer name or field key bumps it. +This vocabulary is versioned as **`tile57/2`**. The tiles and the portrayal +assets are generated from the same S-101 catalogue and stamped with that +`schema_version`, so a renderer can refuse a schema it doesn't speak. Any change +to a layer name or field key bumps it. Every tile uses an extent of **4096** and a buffer of **64**. :::note Live path coverage Whether the tiles come from a pre-baked PMTiles archive or are generated live from -a raw S-57 cell, the schema is the same. The live path (`src/scene/`) -emits `areas`, `area_patterns`, `lines`, `complex_lines`, `point_symbols`, -`soundings`, and `text`, plus the `*_scamin` declutter buckets (features carrying -SCAMIN, attr 133) and a `draw_prio` property for S-52 fill ordering. DEPCNT lines +a raw S-57 chart, the schema is the same. The live path (`src/scene/`) +emits `areas`, `area_patterns`, `lines`, `point_symbols`, `soundings`, and +`text`. Features carrying SCAMIN (attr 133) keep it as the per-feature `scamin` +property, and every feature has a `draw_prio` for S-52 fill ordering. DEPCNT lines carry `valdco` (the contour value, including the 0 m drying line) for line-centre labels. The same schema is encoded as MLT or MVT (see [architecture](./architecture.md)). @@ -39,9 +38,9 @@ regenerating tiles. ## Zoom levels and navigational bands -A nautical chart is not one map at one scale. NOAA compiles each ENC cell for a +A nautical chart is not one map at one scale. NOAA compiles each chart (an ENC *cell*) for a **navigational purpose** — from a wide overview to a close-in berthing plan — and -the right cell to show depends on how far you are zoomed in. Each cell carries a +the right chart to show depends on how far you are zoomed in. Each chart carries a compilation scale (`CSCL`, a `1:N` denominator) that maps to a band, and each band covers the Web-Mercator zoom range that matches its scale: @@ -55,12 +54,30 @@ covers the Web-Mercator zoom range that matches its scale: | Berthing | 16 – 18 | Vector tiles scale crisply, so a renderer **overzooms** the top level rather than -baking more. Within a band, each feature carries an S-52 **SCAMIN** (the scale -below which it should disappear); honoring it per-feature is what the `*_scamin` -buckets are for, so minor features drop out at their own thresholds and the chart -never clutters. +baking more. Within a band, a feature may carry an S-52 **SCAMIN** (the scale +below which it should disappear) as the per-feature `scamin` property; the style +hides the feature once the display is coarser than `1:scamin`, so minor features +drop out at their own thresholds and the chart never clutters. (Earlier schema +versions split these into separate `*_scamin` layers; `tile57/2` folds them back +into the base layers, gated by the per-feature property.) -## The seven layers +## The six layers + +Every feature also carries shared metadata the style and the pick report read, +regardless of layer: + +| Field | Type | Meaning | +| --- | --- | --- | +| `class` | string | S-57 object-class acronym. | +| `cell` | string | Source chart stem. | +| `s57` | string | The feature's full S-57 attribute set as a JSON object (the pick report). | +| `draw_prio` | int | S-52 draw priority (fill/stroke ordering). | +| `cat` | int | Display category (base / standard / other gating). | +| `band` | int | Navigational band rank. | +| `scamin` | int | SCAMIN `1:N` denominator — present only on features that carry one. | + +`bnd`, `pts`, `plane`, `vg`, and the `date_*` keys are additional gating +properties emitted only when relevant. The layer-specific fields follow. ### areas @@ -69,10 +86,8 @@ Filled polygons, such as depth areas and land. | Field | Type | Meaning | | --- | --- | --- | | `color_token` | string | Fill color name. | -| `class` | string | S-57 object class. | -| `draw_prio` | int | Draw priority. | -| `cat` | — | Category. | -| `bnd` | — | Boundary-pass marker. | +| `drval1`, `drval2` | number | Depth-range min/max for depth areas (DEPARE/DRGARE). | +| `oscl` | int | Overscale denominator, present when the chart shows finer than its compilation scale. | ### area_patterns @@ -81,28 +96,21 @@ Polygons filled with a repeating pattern instead of a flat color. | Field | Type | Meaning | | --- | --- | --- | | `pattern_name` | string | Name of the fill pattern. | -| `class` | string | S-57 object class. | -| `draw_prio` | int | Draw priority. | +| `oscl` | int | Overscale denominator, as above. | ### lines -Stroked lines, such as depth contours. Sector-light legs and arcs also go here. +Stroked lines, such as depth contours and coastline. Symbolized/complex line runs +and sector-light legs fold in here too. | Field | Type | Meaning | | --- | --- | --- | -| `class` | string | S-57 object class. | | `color_token` | string | Stroke color name. | -| `width_px` | int | Stroke width in pixels. | -| `dash` | — | Dash pattern. | - -### complex_lines - -Lines drawn with a named, repeating line style. - -| Field | Type | Meaning | -| --- | --- | --- | -| `class` | string | S-57 object class. | -| `linestyle_name` | string | Name of the line style. | +| `width_px` | number | Stroke width in pixels. | +| `dash` | string | Dash pattern (empty = solid). | +| `valdco` | number | Contour value for DEPCNT lines (including the 0 m drying line), for line-centre labels. | +| `ls_style` | string | Complex line-style name (symbolized lines). | +| `ls_arc0` | number | Sector-arc start bearing (sector-light legs). | ### point_symbols @@ -110,24 +118,21 @@ Single symbols placed at a point, such as buoys and beacons. | Field | Type | Meaning | | --- | --- | --- | -| `class` | string | S-57 object class. | | `symbol_name` | string | Name of the symbol. | | `rotation_deg` | number | Rotation in degrees. | +| `rot_north` | int | `1` = hold the symbol north-up (line-placed and point symbols). | | `scale` | number | Scale factor. | -| `offset_x`, `offset_y` | number | Pixel offset from the point. | -| `halo_color_token` | string | Halo color name. | -| `draw_prio` | int | Draw priority. | +| `danger_depth`, `sym_deep` | number, string | Isolated-danger depth + the deep-water symbol variant (the DANGER01/DANGER02 swap). | ### soundings -Depth soundings, drawn as digit symbols (SNDFRM04 digit composition). +Depth soundings, drawn as digit glyph strings (SNDFRM digit composition). | Field | Type | Meaning | | --- | --- | --- | -| `class` | string | S-57 object class. | -| `symbol_names` | string | The digit symbols that make up the sounding. | -| `scale` | number | Scale factor. | -| `draw_prio` | int | Draw priority. | +| `sym_s`, `sym_g` | string | Bold + faint digit glyph strings, in metres. | +| `sym_s_ft`, `sym_g_ft` | string | The same glyph strings, in feet. | +| `depth` | number | Sounding depth in metres. | ### text @@ -136,11 +141,11 @@ attribute). | Field | Type | Meaning | | --- | --- | --- | -| `class` | string | S-57 object class. | | `text` | string | The label text. | | `font_size_px` | number | Font size in pixels. | | `color_token` | string | Text color name. | -| `halign`, `valign` | — | Horizontal and vertical alignment. | -| `offset_x`, `offset_y` | number | Pixel offset from the anchor. | -| `halo_color_token` | string | Halo color name. | -| `draw_prio` | int | Draw priority. | +| `halo_color_token` | string | Halo color name (`""` = no halo). | +| `halo_width` | number | Halo width in pixels (`0` = none). | +| `halign`, `valign` | string | Horizontal and vertical alignment. | +| `loff` | string | Pixel offset `"ux,uy"` from the anchor, present only when nonzero. | +| `tgrp` | int | S-52 text viewing group. | diff --git a/docs/docs/zig-api.md b/docs/docs/zig-api.md index 9cd7230..a1bdbfa 100644 --- a/docs/docs/zig-api.md +++ b/docs/docs/zig-api.md @@ -1,15 +1,17 @@ --- id: zig-api title: Zig API -sidebar_position: 4 +sidebar_position: 5 --- # Zig API -The engine is a Zig package named **`tile57`** (v0.1.0, requires Zig 0.16). +The engine is a Zig package named **`tile57`** (v0.3.0, requires Zig 0.16). Add it as a dependency and `@import("tile57")` for the curated public surface -(`src/tile57.zig`). The [C ABI](./c-api.md) is a thin shim over this same -API. +(`src/tile57.zig`). The [C ABI](./c-api.md) is a thin shim over this same API, +and both share the same shape: **bake, then compose** (or bake, then render) — +source charts bake once to per-chart archives, and every output is produced +from baked archives. :::note Add it as a **path dependency** on a local clone (submodules initialised) — @@ -18,82 +20,138 @@ Add it as a **path dependency** on a local clone (submodules initialised) — [Installation](./installation.md). ::: -## The high-level engine: `Chart` +## Bake -Open a **chart** from bytes, a path, or cells, then fetch tiles by `(z, x, y)`: +Bake each chart to its own PMTiles at its compilation scale — the input the +compositor serves from. Free any returned bytes with `tile57.freeBytes`. + +```zig +// Bake an ENC_ROOT: each chart -> /tiles/.pmtiles + /partition.tpart. +// Incremental: an archive already newer than its whole input is skipped. +const n = try tile57.bake.tree(io, "/enc/ENC_ROOT", "/out", null, 4, null, null); +``` + +| Surface | What it does | +|---------|--------------| +| `tile57.bake.chartBytes(path, rules)` | bake one chart (+ updates) to PMTiles bytes. | +| `tile57.bake.chartsParallel(...)` / `bake.chartsToFiles(...)` | bake many charts in parallel, to memory / to files. | +| `tile57.bake.tree(io, in, out, ...)` | walk an ENC_ROOT, bake each chart to a mirrored path (incremental). | +| `tile57.bake.pmtilesMetadata(a, bytes)` | read an archive's metadata JSON (embedded coverage + scamin). | +| `tile57.bake.Progress` | the optional progress-callback type. | + +Reading raw S-57 source data (a chart inventory, GeoJSON feature extraction) +goes through a streaming `Chart` — see `openPath` below. + +## Render: the `Chart` + +A `Chart` is one open chart: metadata, feature extraction, the S-52 cursor +pick, and view renders — with no composition. Tiles across many charts come +from the compositor (next section). ```zig const tile57 = @import("tile57"); -// A PMTiles archive, or a raw S-57 cell portrayed live (auto-sniffed). -var chart = try tile57.Chart.openBytes(cell_bytes, .auto, null); +// A baked archive, mmap'd (never fully resident). +var chart = try tile57.Chart.openPmtilesPath(io, "US5MD1MC.pmtiles"); defer chart.deinit(); -if (try chart.tile(z, x, y)) |bytes| { // decompressed tile bytes, or null if empty - defer tile57.freeBytes(bytes); - // … hand to your renderer … -} +const bbox = chart.bounds(); // geographic extent [w, s, e, n], or null +// … chart.renderView(…) / chart.queryPoint(…) … ``` +What a chart can do depends on how it was opened: + +| Open | Backend | Serves | +|------|---------|--------| +| `openPmtilesPath(io, path)` | baked archive, mmap'd | metadata (embedded coverage + scale), query, view renders (tile replay), raw tiles via `pmtilesReader()`. | +| `openBytes(bytes, .pmtiles, …)` | baked archive, copied | the same, from memory. | +| `openBytes(cell_bytes, .auto, rules_dir)` | ONE live S-57 chart, fully portrayed | metadata, query, view renders with the S-101 rules evaluated live. | +| `openPath(path, rules_dir, pick_attrs)` | streaming ENC_ROOT (or a single `.000`) | metadata + extraction ONLY: `chartsJson`, `featuresJson`, `scamin`, bounds. Charts are enumerated up front and parsed on demand, so a whole catalogue opens instantly. No view renders, no tiles. | +| `openCharts(charts, …)` / `openChartsStreaming(metas, reader, …)` | the same, from in-memory charts / a host reader callback | as `openPath`. | + `Chart` methods: | Method | Purpose | |--------|---------| -| `openBytes(bytes, format, rules_dir)` | open one chart from bytes (PMTiles or S-57 cell). | -| `openPath(path, rules_dir, pick_attrs)` | open an on-disk ENC_ROOT dir (or a single `.000`) as a streaming chart. | -| `openCells(cells, rules_dir, pick_attrs)` | open a multi-cell ENC_ROOT from bytes (each cell's `.001…` updates applied). | -| `openCellsStreaming(metas, reader, user, rules_dir, pick_attrs)` | low-memory ENC_ROOT: read each cell's bytes on demand, free on eviction. | -| `tile(z, x, y) -> ?[]u8` | the tile's decompressed bytes, in the chart's tile encoding (null = empty). | -| `tileType()` / `setTileFormat(fmt)` | the encoding `tile` returns (MVT/MLT); opt a live cell-backed chart into MLT. | -| `renderView(lon, lat, zoom, w, h, palette, settings, output)` | render a view through the native S-52 pixel path — PNG or PDF. | +| `renderView(lon, lat, zoom, w, h, palette, settings, output, cb)` | render a view through the native S-52 pixel path — PNG, PDF, or a callback canvas. | +| `renderSurfaceView(lon, lat, zoom, w, h, palette, settings, cb)` | drive world-space surface callbacks (the GPU vector twin). | | `renderAscii(lon, lat, zoom, cols, rows, palette, settings, ansi)` | the same view as a terminal text grid. | -| `cellsJson()` / `featuresJson(classes)` | per-cell metadata / GeoJSON feature query (the `cells` / `features` CLI). | +| `queryPoint(lon, lat, zoom, cb)` | the S-52 cursor pick — features under a point at the view zoom. | +| `chartsJson()` / `featuresJson(classes)` | per-chart metadata / GeoJSON feature extraction (the `cells` / `features` CLI). | +| `coverage()` | the M_COVR data-coverage rings (a live chart, or the copy a per-chart bake embeds in its archive metadata). | | `bounds() -> ?[4]f64` | geographic extent `[w, s, e, n]`, if known. | | `anchor()` | a good initial camera (lat, lon, zoom) on real data. | | `bands() -> u32` | bitmask of navigational bands present. | -| `zoomRange()` | the min/max zoom the chart serves. | +| `zoomRange()` | the min/max zoom the chart covers. | +| `nativeScale() -> i32` | the compilation scale 1:N (a live chart or an archive's embedded metadata; 0 if unknown). | | `scamin() -> ![]u32` | the distinct SCAMIN denominators present (the live SCAMIN manifest). | +| `tileType()` | the tile encoding the chart's tiles use (MVT/MLT). | | `format() -> Format` | the resolved backend (after `.auto`). | -| `clearCache()` | drop the in-memory tile cache. | +| `pmtilesReader()` / `decodedCoverage()` | the archive reader (raw per-archive tiles — the primitive for writing your own compositor) + the decoded per-chart coverage; what the built-in compositor borrows. | | `deinit()` | release the chart and its cached tiles. | -`tile57.Format` is `.auto` / `.pmtiles` / `.s57_cell`. `rules_dir` is the S-101 -portrayal rules directory for live S-57 cells; `null` (or `""`) uses the rules +`tile57.Format` is `.auto` / `.pmtiles` / `.s57`. `rules_dir` is the S-101 +portrayal rules directory for live S-57 charts; `null` (or `""`) uses the rules embedded in the binary (or `TILE57_S101_RULES` if set), so no on-disk catalogue -is required; a path overrides with an on-disk catalogue. Free any bytes returned -by `tile` / `bakeArchive` with `tile57.freeBytes`. +is required; a path overrides with an on-disk catalogue. + +The streaming open uses the extern types `tile57.ChartMeta` (bbox + `cscl`), +`tile57.ChartBytes` (the chart's base + updates, ownership transferred to the +library), and `tile57.ChartReadFn` (the reader callback). Multi-chart input for +`openCharts` is `tile57.ChartInput`. -The streaming open uses the extern types `tile57.CellMeta` (bbox + `cscl`), -`tile57.CellBytes` (the cell's base + updates, ownership transferred to the -library), and `tile57.CellReadFn` (the reader callback). Multi-cell input for -`openCells` is `tile57.CellInput`. +## Compose -## Bake an ENC_ROOT +The runtime compositor stitches per-chart archives into one seamless chart: +any `(z, x, y)` tile on demand through the ownership partition (charts never +double-draw where they meet), and the same view outputs as a single chart, +composed. ```zig -// Band-streamed: peak memory tracks the largest single band, not the whole archive. -const pmtiles_bytes = try tile57.bakeArchive(/* … */); -defer tile57.freeBytes(pmtiles_bytes); +// Open the compositor over the archives + partition, then compose tiles. +var src = (try tile57.compose.ComposeSource.openFiles(io, gpa, paths, "/out/partition.tpart")).?; +defer src.deinit(); +const result = try src.tile(gpa, 13, 2359, 3139); // result.tile: ?[]u8, result.owned: bool + +// The composed view outputs live beside the Chart ones: +const png = try tile57.compose.renderView(src, lon, lat, 13.5, 1600, 1200, .day, &settings, .png, null); ``` -`tile57.bakeArchive` bakes a whole ENC_ROOT into one PMTiles archive, zoom-banded -per cell by compilation scale; `tile57.Progress` is the optional progress -callback type. +A host that already holds open charts composes over them instead — the +compositor borrows each chart's mmap'd reader + decoded coverage, so the charts +must outlive it: + +```zig +const archives = [_]tile57.compose.ChartArchive{ + .{ .reader = chart.pmtilesReader().?, .cov = chart.decodedCoverage().? }, +}; +var src = (try tile57.compose.ComposeSource.open(gpa, &archives, null)).?; +``` + +| Surface | What it does | +|---------|--------------| +| `ComposeSource.openFiles(io, gpa, paths, part)` | open a `ComposeSource` over on-disk archives + a partition. | +| `ComposeSource.open(gpa, archives, part)` | the same over borrowed `ChartArchive`s (already-open charts). | +| `ComposeSource.tile(gpa, z, x, y)` | compose one tile on demand (raw MLT + the ownership flag). | +| `tile57.compose.renderView(src, ...)` | the composed view render — PNG, PDF, or a callback canvas. | +| `tile57.compose.renderSurfaceView(src, ...)` | the composed world-space surface stream. | +| `tile57.compose.queryPoint(src, lon, lat, zoom, cb)` | the composed cursor pick, across chart boundaries. | +| `tile57.compose.tile(...)` | the stateless core `ComposeSource.tile` uses. | +| `tile57.partition` | the ownership partition and its `.tpart` sidecar (serialize / deserialize). | ## Style + portrayal assets ```zig // MapLibre style from a template + mariner S-52 display settings + colortables. -const json = try tile57.style.build(/* … */); // tile57.style.Mariner settings +const json = try tile57.style.buildFromTemplate(/* … */); // tile57.Mariner settings ``` | Surface | What it does | |---------|--------------| -| `tile57.style.build` (`chartstyle.buildStyle`) | patch a MapLibre style template with mariner settings + colortables. | -| `tile57.style.Mariner` | the S-52 mariner display options struct. | -| `tile57.assets` | colortables / linestyles / style.json / manifest generation. | +| `tile57.style` | the MapLibre style: `json`, `Options`, `diff`, `buildFromTemplate`, color tables, line styles. | +| `tile57.style.mariner` | the S-52 mariner settings model and expression builders. | +| `tile57.Mariner` | the S-52 mariner display options struct (`style.mariner.Settings`). | | `tile57.sprite` | S-101 sprite + area-fill pattern atlases (SVG raster). | -| `tile57.chartstyle` | the mariner-driven style-patching module. | ## Tiling + encoding @@ -101,11 +159,13 @@ The mid-level packages, for callers that compose their own pipeline: | Module | Role | |--------|------| -| `tile57.mvt` | Mapbox Vector Tile encode/decode | +| `tile57.mvt` / `tile57.mlt` | Mapbox Vector Tile / MapLibre Tile encode/decode | | `tile57.tile` | web-mercator tiling + clipping | | `tile57.pmtiles` | PMTiles read/write | -| `tile57.bake_enc` | banded multi-cell ENC_ROOT → PMTiles | +| `tile57.band` | compilation-scale → zoom-range mapping | +| `tile57.bake_enc` | banded multi-chart ENC_ROOT → PMTiles | | `tile57.scene` | S-57 feature → tile-surface scene generation | +| `tile57.render` | the Surface/Canvas rendering path (PNG, PDF, ASCII, callbacks) | ## Raw formats (advanced) @@ -114,8 +174,9 @@ The pure-Zig foundational parsers under `tile57.formats`: | Module | Role | |--------|------| | `tile57.formats.iso8211` | ISO/IEC 8211 records | -| `tile57.formats.s57` | S-57 ENC cell parser + geometry | -| `tile57.formats.s100` | S-100/S-101 catalogue + adaptation | +| `tile57.formats.s57` | the S-57 chart parser + geometry | +| `tile57.formats.s101` | the S-101 catalogue, adapter, and instruction stream | -`tile57.version` is the package version string (`"0.1.0"`), matching -`build.zig.zon` and `tile57_version()`. +`tile57.coverage` is the per-chart M_COVR coverage sidecar (carried in an +archive's PMTiles metadata). `tile57.version` is the package version string +(`"0.3.0"`), matching `build.zig.zon` and `tile57_version()`. diff --git a/docs/sidebars.js b/docs/sidebars.js index f52a014..33ed45e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -6,6 +6,7 @@ const sidebars = { 'intro', 'installation', 'getting-started', + 'cli', 'zig-api', 'c-api', 'architecture', diff --git a/include/tile57.h b/include/tile57.h index eb21460..2d9ad71 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -1,25 +1,63 @@ /* tile57.h — public C ABI for libtile57. * - * libtile57 is an embeddable nautical-chart tile engine. You open a chart - * from a path or in-memory bytes (a PMTiles archive or a raw S-57 ENC cell) and it - * serves decompressed vector tiles by (z, x, y) — MapLibre Tiles (MLT, the default - * bake format) or Mapbox Vector Tiles (MVT), per tile57_chart_info.tile_type. The - * bytes are produced by the Zig tile generator (the engine/ sources) and consumed - * by any matching renderer — maplibre-gl >= 5.12 decodes both natively (vector - * source `encoding` option) — but the ABI itself is renderer-agnostic. + * libtile57 is an embeddable nautical-chart engine. It reads IHO S-57 ENC + * charts (each one a *cell*, in the spec's vocabulary), bakes them to per-chart + * PMTiles archives, serves composed vector tiles from + * those archives on demand, renders finished charts to pixels / PDF / a callback + * surface, and generates the matching S-52 MapLibre style + portrayal assets. * - * Lifetime: a tile57_chart must OUTLIVE every renderer/adapter still holding it. - * In the MapLibre hosts the chart is captured by a long-lived FileSource and - * is intentionally never closed before process exit (closing first would be a - * use-after-free during Map teardown). Call tile57_chart_close only once nothing - * can still call tile57_chart_tile on it. + * Everything is BAKE, THEN COMPOSE (or bake, then render): source charts bake + * once to per-chart archives; every output — a tile, a PNG, a PDF, a callback + * stream — is produced from baked archives. The header is organised the same + * way: * - * Threading: a tile57_chart is NOT internally synchronized. Do not call into the - * same chart from multiple threads concurrently (the tile cache is mutated by - * tile57_chart_tile without a lock). Distinct charts are independent. + * 3. BAKE ENC source data in, per-chart PMTiles out. Each chart bakes to + * its own archive at its own compilation scale, with its M_COVR + * coverage + scale embedded in the archive metadata. This is the + * import step; the section also carries the raw-source readers + * (chart inventory, feature extraction, exchange-set catalogue). * - * Memory: tile57_chart_tile allocates *out; release it with tile57_free, passing the - * same length. All pointers are POD across the seam. + * 4. RENDER a `tile57_chart` handle opens ONE baked archive (mmap'd from a + * path, or copied from bytes) and answers for it with NO + * composition: metadata (info / scamin / coverage), its stored + * tiles verbatim (tile57_chart_tile — the primitive for writing your + * own compositor), the cursor pick (query), and view outputs + * (png / pdf / canvas / surface). + * + * 5. COMPOSE a `tile57_compose` handle stitches MANY open charts through the + * ownership partition and offers the SAME output set, + * composed: tile (what a live tile server hands its HTTP layer), + * png, pdf, canvas, surface, query. + * + * Section 6 (style + portrayal assets) turns the mariner's S-52 display options + * into a concrete MapLibre style JSON plus the colortables / sprite / pattern / + * glyph atlases it references. The composed tiles are MapLibre Tiles (MLT); + * maplibre-gl >= 5.12 decodes them natively via the vector source `encoding` + * option. The ABI is renderer-agnostic. + * + * Errors: every call that can fail returns a tile57_status (TILE57_OK = 0) and takes + * an optional caller-owned tile57_error* it fills on failure — see section 2. + * Out-parameters are always defined on return: the result on TILE57_OK, + * NULL/0 otherwise. "Nothing produced" is NOT a failure — a call that finds + * nothing (a chart that bakes no tiles, a tile nobody owns, a chart with no + * SCAMIN values) returns TILE57_OK with a NULL/zero out. + * + * Lifetime: a handle must OUTLIVE every borrower still holding it: a compositor + * borrows its charts (close the compositor first, then the charts), and in + * the MapLibre hosts a long-lived source captures the handle and intentionally + * never closes it before process exit (closing first would be a use-after-free + * during teardown). A path-opened chart mmaps its file — the file must stay in + * place while the chart is open. + * + * Threading: no handle is internally synchronized — do not call into the SAME + * handle from multiple threads concurrently (caches are mutated without a + * lock). A compositor reads through its charts, so while it serves, do not + * call those charts' own methods from other threads. Distinct handles are + * independent. + * + * Memory: calls that return bytes allocate *out; release it with + * tile57_free(ptr) — buffers are length-prefixed at allocation, so the + * pointer is all it needs. All pointers are POD across the ABI. * * The S-101 portrayal self-test / bring-up entry points live in a separate * header, tile57_diag.h (developer tooling, not part of the embedding API). @@ -35,276 +73,368 @@ extern "C" { #endif -/* Library version. tile57_version() returns the string form, e.g. "0.1.0". */ +/* ======================================================================== * + * 1. Version + * ======================================================================== */ + +/* Library version. tile57_version() returns the string form, e.g. "0.3.0". */ #define TILE57_VERSION_MAJOR 0 -#define TILE57_VERSION_MINOR 1 +#define TILE57_VERSION_MINOR 3 #define TILE57_VERSION_PATCH 0 const char *tile57_version(void); -/* Opaque chart handle. */ -typedef struct tile57_chart tile57_chart; - -/* One ENC cell for tile57_bake_pmtiles: the base .000 bytes plus its - * sequential update files (.001, .002, … in order). `updates`/`update_lens` are - * parallel arrays of length `update_count`; pass update_count = 0 (and NULL - * arrays) for a base-only cell. All bytes are copied. +/* ======================================================================== * + * 2. Errors + * + * A call that can fail returns a tile57_status; TILE57_OK (0) is success and every + * other value is a failure with a coarse cause. Results come back through + * out-parameters, which are always defined on return (NULL/0 on failure). * - * `name` is the source cell name (e.g. "US4MD81M"), emitted as the `cell` pick- - * report property on every feature from this cell (see the pick attributes below). - * NULL/"" = omit it. The field is appended last, so a host that zero-inits the - * struct (or was compiled before this field existed) gets the NULL (no-badge) - * behaviour and stays ABI-compatible for the original fields. */ + * tile57_status_str() gives a static, human-readable string for a status (the + * strerror pattern). Where the specific cause is worth carrying — which path, + * which parse failure — the call also takes an optional tile57_error*: pass + * NULL to ignore it, or a caller-owned struct (a stack local is fine) the call + * fills with the status + a message on failure and leaves untouched on + * success. There is no allocation and nothing to free. + * ======================================================================== */ + +typedef enum { + TILE57_OK = 0, /* success */ + TILE57_ERR_BADARG, /* a NULL or out-of-range argument */ + TILE57_ERR_IO, /* a file/directory could not be opened, read, or written */ + TILE57_ERR_PARSE, /* malformed input (S-57 chart, PMTiles, partition, JSON) */ + TILE57_ERR_NOMEM, /* an allocation failed */ + TILE57_ERR_UNSUPPORTED, /* valid but unusable input (e.g. no coverage-carrying chart) */ + TILE57_ERR_RENDER, /* tile generation or rendering failed */ + TILE57_ERR_INTERNAL, /* an unexpected engine failure (please report) */ +} tile57_status; + +/* A static, human-readable string for a status. Never NULL, never freed. */ +const char *tile57_status_str(tile57_status status); + +#define TILE57_ERROR_MSG_MAX 256 typedef struct { - const uint8_t *base; - size_t base_len; - const uint8_t *const *updates; - const size_t *update_lens; - size_t update_count; - const char *name; -} tile57_cell; - -/* ---- pick-report attributes --------------------------------------------- + tile57_status status; + char message[TILE57_ERROR_MSG_MAX]; /* NUL-terminated; "" when no detail */ +} tile57_error; + +/* ======================================================================== * + * 3. Bake + * + * ENC source data in, per-chart PMTiles archives out. The composite model bakes + * each chart to its own archive over its NATIVE band zoom range at its own + * compilation scale; the compositor (section 5) stitches them on demand, so + * baking never composes and re-importing one chart never re-bakes the rest. + * + * A per-chart archive's metadata embeds the chart's M_COVR coverage, compilation + * scale, and identity — everything a chart handle or compositor needs to place + * it, with no .000 re-parse (read it back with tile57_pmtiles_metadata). * - * Every emitted MVT feature carries the per-feature cursor-pick / inspector - * properties used by the S-52 §10.8 pick report: `class` (object-class acronym), - * `cell` (source cell name, from tile57_cell.name / the bundle's file stem), - * and `s57` (a JSON object string of the feature's full S-57 attribute set, - * acronym -> value). These default ON. The `s57` blob is the bulk of a feature's - * size, so a lean deployment that doesn't need pick/inspect can drop ALL three by - * passing omit_pick_attrs != 0 to the open/bake call below. omit_pick_attrs == 0 - * (the zero-initialised default) keeps them — so existing callers that pass 0 get - * the pick report for free. */ - -/* Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the - * 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); - -/* 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. */ -tile57_chart *tile57_charts_open(const char *path); + * Every emitted vector-tile feature carries the per-feature pick/inspector + * properties used by the S-52 §10.8 pick report: `class` (object-class + * acronym), `cell` (source chart name), and `s57` (a JSON object of the + * feature's full S-57 attribute set, acronym -> value). tile57_chart_query and a + * host inspector read these back. + * + * The section also carries the raw-source readers a host's import UI needs: + * the chart inventory of a path, GeoJSON feature extraction, and the + * exchange-set catalogue decode. + * ======================================================================== */ -/* 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); +/* ---- reading ENC source data --------------------------------------------- */ + +/* The per-chart metadata of the S-57 data at `path` — ONE chart (a .000 file, + * with its .001.. update chain auto-read from the same directory) or a whole + * ENC_ROOT directory — as a JSON array, one object per chart: + * [{"name":"US5MD1MC","scale":12000,"edition":"13","update":"3", + * "issueDate":"20240105","agency":550,"bbox":[west,south,east,north]}, ...] + * `name` is the DSNM stem; `scale` is DSPM CSCL; edition/update/issueDate/ + * agency are DSID EDTN/UPDN/ISDT/AGEN after the update chain is applied; + * `bbox` is omitted when none parses. For a host's chart-database scan. + * TILE57_OK with the JSON in *out / *out_len (free with tile57_free). */ +tile57_status tile57_enc_charts(const char *path, uint8_t **out, size_t *out_len, + tile57_error *err); + +/* The features of the S-57 data at `path` (one chart or a whole ENC_ROOT) for + * the given object classes (comma-separated acronyms, e.g. "DEPARE,DRGARE") as + * a GeoJSON FeatureCollection: geometry in lon/lat (Polygon rings + * largest-first, MultiPoint with depths for soundings, LineString/Point as + * encoded), properties = {"class":"DEPARE", ...the feature's full S-57 + * acronym->value attribute map}. Parsed without portrayal; a whole-ENC_ROOT + * extraction walks every chart — the caller owns that cost. TILE57_OK with the + * JSON in *out / *out_len (free with tile57_free); NULL/0 when nothing + * matched. */ +tile57_status tile57_enc_features(const char *path, const char *classes, + uint8_t **out, size_t *out_len, + tile57_error *err); + +/* tile57_enc_features over in-memory base .000 bytes (read from a zip member, + * say) instead of a path. No update chain is applied. */ +tile57_status tile57_enc_features_bytes(const uint8_t *base, size_t len, + const char *classes, + uint8_t **out, size_t *out_len, + tile57_error *err); + +/* Decode a CATALOG.031 exchange-set catalogue (raw bytes) into a JSON array of + * its CATD entries: + * [{"file":"US5MD1MC/US5MD1MC.000","longName":"Annapolis Harbor", + * "impl":"BIN","bbox":[west,south,east,north]}, ...] + * `file` is the recorded path with separators normalised to '/'; `longName` is + * LFIL (the human chart title; empty when absent); `impl` is BIN/ASC/TXT; + * `bbox` is omitted when SLAT/WLON/NLAT/ELON are not all present (aux files). + * TILE57_OK with the JSON in *out / *out_len (free with tile57_free); NULL/0 + * when the file holds no CATD records. */ +tile57_status tile57_enc_catalog(const uint8_t *catalog_031, size_t len, + uint8_t **out, size_t *out_len, + tile57_error *err); + +/* ---- baking charts to per-chart archives ---------------------------------- */ + +/* Bake ONE chart (+ its .001.. updates, read from disk) to PMTiles bytes over + * its NATIVE band zoom range and nothing else. Returned in *out / *out_len + * (free with tile57_free); NULL/0 when the chart produced no tiles. For a host + * persisting a per-chart tile cache to disk — open the archives as charts + * (section 4) and compose them (section 5). */ +tile57_status tile57_bake_chart_bytes(const char *path, uint8_t **out, size_t *out_len, + tile57_error *err); + +/* Bake `n` charts (each a .000 path; updates auto-read) to per-chart PMTiles + * bytes IN PARALLEL across up to `workers` threads. The engine returns BYTES + * only — it never writes an output directory; the host writes each archive + * into the cache it manages. out_bytes[i] / out_lens[i] receive chart i's + * archive (free each with tile57_free) or NULL/0 when that chart produced + * nothing; both arrays are caller-allocated, length n. *out_baked (NULL to + * ignore) receives the number of charts that produced bytes. `workers` is a + * MEMORY bound — each concurrent bake holds a whole chart's + * parse+portray+raster working set, so pass a small count (not a core count). + * Warms up the process globals internally, so concurrent baking is race-free. */ +tile57_status tile57_bake_charts(const char *const *paths, size_t n, uint32_t workers, + uint8_t **out_bytes, size_t *out_lens, + size_t *out_baked, tile57_error *err); + +/* Progress callback for tile57_bake_tree: fires after each chart with (done, + * total). May be called CONCURRENTLY from worker threads — make it + * thread-safe. */ +typedef void (*tile57_bake_progress)(void *ctx, uint32_t done, uint32_t total); + +/* Walk `in_dir` for S-57 base charts (*.000) and bake each IN PARALLEL to the + * SAME relative path under `out_dir` with a .pmtiles extension + * (in_dir/d1/US4CT1AA.000 -> out_dir/d1/US4CT1AA.pmtiles), plus an .sha + * content-hash sidecar; output subdirs are created as needed. The engine + * writes and frees each archive as it goes, so the host never holds N archives + * in memory (peak ~ workers). INCREMENTAL: a chart whose mirrored archive is + * already at least as new as its whole input (.000 + update chain) is skipped, + * so a re-run over an unchanged tree bakes nothing — *out_baked (NULL to + * ignore) counts the charts baked THIS run, and 0 over a warm cache is success, + * not failure. `in_dir` is the source ENC data; `out_dir` is the caller's OWN + * cache — it owns the location + layout, so distinct library consumers each + * keep their own chart library without clashing. `workers` is a MEMORY bound — + * pass a small count. An unreadable `in_dir` is TILE57_ERR_IO. */ +tile57_status tile57_bake_tree(const char *in_dir, const char *out_dir, uint32_t workers, + tile57_bake_progress progress, void *progress_ctx, + uint32_t *out_baked, tile57_error *err); + +/* Read a PMTiles archive's metadata JSON blob (decompressed) into *out / + * *out_len (free with tile57_free); NULL/0 when the archive carries none. A + * per-chart bake embeds the chart's M_COVR coverage + cscl + date/name under a + * "coverage" key. */ +tile57_status tile57_pmtiles_metadata(const uint8_t *pmtiles, size_t len, + uint8_t **out, size_t *out_len, + tile57_error *err); + +/* Bake the ownership-partition DEBUG tiles from an ENC_ROOT (on-disk path) + * into a single PMTiles at out_path: the composited ownership faces (which + * chart renders which ground at each band), one polygon per owning chart tagged + * with the properties cell/cscl/band/tier/oi/color, and NO portrayed chart + * content — for building a partition-debug UI. band < 0 emits the band + * GOVERNING each zoom (the natural view); 0..5 (berthing..overview) emits one + * band's own map at every zoom. minzoom/maxzoom bound the tiles (harbor-level + * detail needs maxzoom >= 13; coarser bands are much cheaper). TILE57_OK with + * *out_chart_count (NULL to ignore) = the covered-chart count; 0 with no file + * written when nothing is covered. */ +tile57_status tile57_bake_partition_debug(const char *enc_root, const char *out_path, + uint8_t minzoom, uint8_t maxzoom, int8_t band, + uint32_t *out_chart_count, tile57_error *err); + +/* ======================================================================== * + * 4. Render + * + * A `tile57_chart` is ONE baked PMTiles archive, opened for metadata and + * output — with NO composition (the compositor, section 5, offers the same + * outputs across many charts). Open it from a path (mmap'd — a whole chart + * library can be open without being resident) or from bytes (copied). It + * reports the archive's zoom range / bounds / tile encoding, the coverage + + * compilation scale the bake embedded, and the live SCAMIN manifest; hands + * back its stored tiles verbatim (the primitive an embedder's own compositor + * consumes); answers the S-52 cursor pick; and renders any VIEW (centre + + * fractional zoom + pixel size) as a finished PNG, a vector PDF, resolved + * pixel-space draw calls, or a world-space semantically-tagged stream. + * + * Rendering REPLAYS baked tiles through the native S-52 pixel path: one whole + * scene across every covering tile, labels decluttered over the full canvas + * (no tile boundaries), symbols replayed as vectors from the catalogue. The + * mariner's live-swappable settings — colour scheme, safety contour + * danger/sounding swaps, category/SCAMIN/text gates, size scale — re-evaluate + * at render time; the rest of the portrayal context was fixed at bake time. + * ======================================================================== */ + +/* Opaque chart handle: one open baked archive. */ +typedef struct tile57_chart tile57_chart; -/* 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); +/* Open a baked PMTiles archive from a file path, mmap'd (never fully + * resident). The file must stay in place while the chart is open. TILE57_OK + * with *out set (close with tile57_chart_close). */ +tile57_status tile57_chart_open(const char *path, tile57_chart **out, tile57_error *err); -/* Open a baked PMTiles bundle from a file path. NULL/failure -> NULL. */ -tile57_chart *tile57_chart_open_pmtiles(const char *path); +/* Open a baked PMTiles archive from in-memory bytes (e.g. straight from + * tile57_bake_chart_bytes, before any file exists). Bytes are copied. */ +tile57_status tile57_chart_open_bytes(const uint8_t *pmtiles, size_t len, + tile57_chart **out, tile57_error *err); -/* Tile encodings served by tile57_chart_tile / produced by the bakes. */ +/* Vector-tile encodings an archive can store (reported in + * tile57_info.tile_type; the engine bakes MLT). */ typedef enum { TILE57_TILE_TYPE_MVT = 1, /* Mapbox Vector Tile */ - TILE57_TILE_TYPE_MLT = 2, /* MapLibre Tile (the default bake format) */ + TILE57_TILE_TYPE_MLT = 2, /* MapLibre Tile (the bake default) */ } tile57_tile_type; -/* Fixed chart metadata — folds zoom_range/bands/bounds/anchor into one - * getter. Bounds/anchor validity are flagged (false -> those fields are 0). - * tile_type is the encoding tile57_chart_tile returns (a tile57_tile_type): a - * PMTiles-backed chart reports its archive's stored type; a cell-backed chart - * reports its live generation format (see tile57_chart_set_tile_format). */ +/* Fixed chart metadata. Bounds/anchor validity are flagged (false -> those + * fields are 0). tile_type is the archive's stored encoding (a + * tile57_tile_type); a host passes it to tile57_style_template so the renderer + * decodes the tiles correctly. native_scale is the compilation scale (1:N) the + * bake embedded in the archive metadata; 0 = unknown (a composed/foreign + * archive — derive the scale from the zoom band instead). */ typedef struct { uint8_t min_zoom, max_zoom; uint32_t bands; /* bitmask of navigational bands present */ 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); - -/* Progress callback for tile57_bake_pmtiles / tile57_bake_bundle. stage 0 = - * loading/portraying cells, stage 1 = baking tiles. Stage 0 done/total count cells. - * Stage 1 done/total count tiles: for tile57_bake_bundle they are PER BAND (reset - * each band) with total = that band's planned tile count, so a host can draw a - * per-band percentage (the count is a planned estimate from cell bboxes — the actual - * emitted total is a little lower as empty tiles are skipped, like the Go baker's - * planned bar). total 0 means "unknown" (the tile57_bake_pmtiles path with no planned - * count). - * - * band_index / band_count locate the current band among the bands that actually - * bake (0-based index; band_count = how many bands have cells), so the host can - * label "band /". band_name is the band's - * navigational-purpose name ("berthing","harbor","approach","coastal","general", - * "overview") as a static NUL-terminated string owned by the library (valid for the - * call; copy if retained), or NULL during a non-band-specific report. Together they - * let the host show e.g. "Generating approach tiles (band 3/6): 84/427 (20%)". */ -typedef void (*tile57_bake_progress)(void *user, uint8_t stage, size_t done, size_t total, - uint8_t band_index, uint8_t band_count, - const char *band_name); - -/* Shared bake options for tile57_bake_pmtiles / tile57_bake_bundle. Pass NULL for - * all defaults (embedded rules/catalogue, no band clamp, pick attrs included, no - * progress). `catalog_dir` and `created` apply to tile57_bake_bundle only — - * tile57_bake_pmtiles ignores them. */ + int32_t native_scale; +} tile57_info; +void tile57_chart_get_info(tile57_chart *chart, tile57_info *out); + +/* The distinct SCAMIN denominators present in the chart (read from the archive + * metadata), ascending — the host publishes these so its style builds one + * native fractional-minzoom bucket layer per value (features honour their 1:N + * min-display-scale at zero per-zoom cost). TILE57_OK with *out pointing at + * *out_len int32 values, or NULL/0 when the chart has none. Free *out with + * tile57_free. */ +tile57_status tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len, + tile57_error *err); + +/* The chart's M_COVR(CATCOV=1) data-coverage polygons, from the coverage the + * bake embedded in the archive metadata — the real coverage a host reports so + * a quilt fills gaps to coarser charts (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). A chart whose archive embeds no + * coverage (a composed/foreign archive) is TILE57_OK with no calls. */ typedef struct { - const char *rules_dir; /* NULL = embedded portrayal rules */ - const char *catalog_dir; /* NULL = embedded S-101 catalogue (bundle only) */ - const char *created; /* NULL = manifest "created" unset (bundle only) */ - uint8_t minzoom, maxzoom; /* 0/0 = no band clamp */ - bool omit_pick_attrs; - tile57_bake_progress progress; - void *progress_user; /* NULL = default/none */ - uint8_t format; /* baked tile encoding: 0 = default (mlt), - * TILE57_TILE_TYPE_MVT, TILE57_TILE_TYPE_MLT. - * Honored by tile57_bake_pmtiles AND - * tile57_bake_bundle. Appended last, so a - * zero-initialised struct bakes the default. */ -} tile57_bake_opts; - -/* Bake an ENC_ROOT's in-memory cells into ONE PMTiles archive, zoom-banded per cell - * by compilation scale, so the result opens cheaply (tile57_chart_open_pmtiles) - * instead of holding every cell live. `opts` may be NULL for all defaults; it reads - * rules_dir / minzoom / maxzoom / omit_pick_attrs / progress / progress_user (the - * catalog_dir and created fields are ignored — they apply to tile57_bake_bundle). - * minzoom/maxzoom of 0/0 leave the per-cell bands unclamped. On success returns 1 - * with the archive in *out and *out_len (free with tile57_free); 0 if nothing - * covered; -1 on error. Like the live open, this parses + portrays every cell, so - * peak memory tracks the ENC_ROOT size; run it once and cache the archive. */ -int tile57_bake_pmtiles(const tile57_cell *cells, size_t count, - const tile57_bake_opts *opts, - uint8_t **out, size_t *out_len); - -/* Bake a single cell.000 OR a whole ENC_ROOT directory (`input`, an on-disk path) - * into a self-contained chart bundle written under `out_dir` — the SAME package the - * `tile57 bake … -o out_dir/` CLI emits: - * out_dir/tiles/chart.pmtiles (PMTiles with scamin + vector_layers metadata) - * out_dir/assets/colortables.json, linestyles.json, sprite-mln{,@2x}.{json,png} - * out_dir/assets/style-{day,dusk,night}.json (per-scheme, SCAMIN-bucketed) - * out_dir/manifest.json (schema_version, bbox, cells, styles) - * The host registers chart.pmtiles and serves style-*.json verbatim. `opts` may be - * NULL for all defaults, else it reads every field: rules_dir/catalog_dir NULL or "" - * use the catalogue embedded in the library (no on-disk catalogue needed), a - * non-empty path overrides from disk; created NULL/"" leaves the manifest "created" - * unset, else stamps it (e.g. an ISO8601 string); minzoom/maxzoom of 0/0 leave the - * per-cell bands unclamped; progress NULL uses a built-in console progress. - * `out_cell_count` and `out_bbox` (filled west,south,east,north) are optional — pass - * NULL to skip. Returns 1 on success, 0 if nothing was covered (no geometry), -1 on - * error. */ -int tile57_bake_bundle(const char *input, const char *out_dir, - const tile57_bake_opts *opts, - uint32_t *out_cell_count, double *out_bbox); - -/* All portrayal assets in memory (the same files bake_bundle writes to disk), from the - * library's embedded catalogue (catalog_dir NULL/"") or an on-disk one. Pairs with - * tile57_bake_pmtiles + tile57_build_style for a full in-memory bundle. Returns 1 with - * *out filled (free with tile57_assets_free), 0 on error. */ + void *ctx; + void (*ring)(void *ctx, const double *lonlat, size_t npts); +} tile57_coverage_cb; +tile57_status tile57_chart_coverage(tile57_chart *chart, const tile57_coverage_cb *cb, + tile57_error *err); + +/* The chart's own stored tile at (z,x,y), decompressed (MLT or MVT per + * tile57_info.tile_type), with NO composition — the per-archive primitive for + * an embedder writing its own compositor. TILE57_OK with the bytes in *out / + * *out_len (free with tile57_free); NULL/0 when the archive has no tile + * there. */ +tile57_status tile57_chart_tile(tile57_chart *chart, uint8_t z, uint32_t x, uint32_t y, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* Cursor object-query (S-52 §10.8 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 chart name. Pointers are valid only for + * the duration of the call. */ typedef struct { - uint8_t *colortables; size_t colortables_len; - uint8_t *linestyles; size_t linestyles_len; - uint8_t *sprite_json; size_t sprite_json_len; uint8_t *sprite_png; size_t sprite_png_len; - 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); -/* 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); + void *ctx; + void (*feature)(void *ctx, const char *cls, size_t cls_len, + const char *s57, size_t s57_len, + const char *chart, size_t chart_len); +} tile57_query_cb; +/* `zoom` is the current view's web-mercator zoom: the query reads the tile at + * that zoom, so it reports the features actually DISPLAYED there + * (SCAMIN-bucketed) and the pick tolerance tracks on-screen distance. */ +tile57_status tile57_chart_query(tile57_chart *chart, double lon, double lat, double zoom, + const tile57_query_cb *cb, tile57_error *err); -/* Release a chart and all cached tiles. Must not be called while any renderer - * may still call tile57_chart_tile on it (see lifetime note above). */ -void tile57_chart_close(tile57_chart *chart); +/* ---- mariner settings ------------------------------------------------------ + * + * The mariner's S-52 display options: shared by the view renderers below + * (evaluated live per render) and the style builders in section 6 (baked into + * the style JSON). Fill it from your UI, or start from + * tile57_mariner_defaults. */ -/* The distinct SCAMIN denominators present in the chart (the live SCAMIN manifest), - * ascending — the host publishes these so its style builds one native fractional- - * minzoom bucket layer per value (so features honour their 1:N min-display-scale at - * zero per-zoom cost). A PMTiles chart reads them from the archive metadata; a cell - * / ENC_ROOT chart scans every cell's features (parsed without portrayal; streamed - * cells are read transiently). On success returns 1 with *out pointing at *out_len - * int32 values; 0 if there are none; -1 on error. Free *out with - * tile57_free((uint8_t*)*out, *out_len * sizeof(int32_t)). */ -int tile57_chart_scamin(tile57_chart *chart, int32_t **out, size_t *out_len); - -/* Result of tile57_chart_tile. */ -typedef enum { - TILE57_TILE_OK = 1, /* *out / *out_len set (free with tile57_free) */ - TILE57_TILE_EMPTY = 0, /* tile is valid but has no features */ - TILE57_TILE_ERROR = -1, /* generation / decode failure */ -} tile57_tile_status; - -/* Fetch tile (z, x, y) as decompressed vector-tile bytes, VERBATIM in the chart's - * tile encoding — discoverable via tile57_chart_info.tile_type: a PMTiles-backed - * chart returns the stored bytes as baked (MLT by default, MVT for a --format mvt - * bake); a cell-backed chart generates in its live format (see - * tile57_chart_set_tile_format). There is NO transcode layer — hosts hint the - * encoding to the renderer instead (maplibre-gl >= 5.12 decodes MLT natively via - * the vector source `encoding` option). Results are cached per chart, so - * re-requesting a tile (as renderers do) is cheap and deterministic. */ -tile57_tile_status tile57_chart_tile(tile57_chart *chart, uint8_t z, uint32_t x, uint32_t y, - uint8_t **out, size_t *out_len); - -/* Select the encoding for LIVE-generated tiles on a cell-backed chart: 0 = engine - * default (mlt), TILE57_TILE_TYPE_MVT, TILE57_TILE_TYPE_MLT. Cell-backed charts - * OPEN generating MVT (so existing MVT-only embedders are unaffected); a host - * whose renderer decodes MLT opts in here. No-op for a baked PMTiles chart (its - * stored encoding is fixed). Changing the format drops the tile cache. */ -void tile57_chart_set_tile_format(tile57_chart *chart, uint8_t format); - -/* Render a VIEW of the chart — centre + fractional zoom + pixel size — to a - * PNG through the native S-52 pixel path: the mariner settings evaluate LIVE - * (real safety contour + category/SCAMIN/text-group gates + day/dusk/night - * palette), catalogue symbols replay as vectors, and labels declutter over - * the whole canvas (no tile seams). `m` NULL = canonical defaults (see - * tile57_mariner_defaults; declared below). Physical calibration / @2x is - * m->size_scale. Returns 0 with *out and *out_len set (free with tile57_free); - * -1 bad handle, -2 render failure, -3 unsupported source (a baked PMTiles - * chart carries no portrayal to render from). */ -struct tile57_mariner; /* fwd; defined in the chart-style section below */ -int tile57_chart_render_view(tile57_chart *chart, double lon, double lat, double zoom, - uint32_t width, uint32_t height, - const struct tile57_mariner *m, - uint8_t **out, size_t *out_len); +typedef enum { TILE57_SCHEME_DAY=0, TILE57_SCHEME_DUSK=1, TILE57_SCHEME_NIGHT=2 } tile57_scheme; +typedef enum { TILE57_DEPTH_METERS=0, TILE57_DEPTH_FEET=1 } tile57_depth_unit; +typedef enum { TILE57_BOUNDARY_SYMBOLIZED=0, TILE57_BOUNDARY_PLAIN=1 } tile57_boundary_style; -/* tile57_chart_render_view's vector twin: the SAME scene as a deterministic - * single-page PDF (1 px = 1 pt, 72 dpi; vector fills + native strokes + - * glyph-outline text). Same parameters, returns, and ownership. */ -int tile57_chart_render_pdf(tile57_chart *chart, double lon, double lat, double zoom, - uint32_t width, uint32_t height, - 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 tile57_mariner { + tile57_scheme scheme; + double shallow_contour, safety_contour, deep_contour, safety_depth; + bool four_shade_water; + tile57_depth_unit depth_unit; + bool display_base, display_standard, display_other; + bool data_quality, show_inform_callouts, show_meta_bounds, show_isolated_dangers_shallow; + tile57_boundary_style boundary_style; + bool simplified_points, show_full_sector_lines; + bool text_names, show_light_descriptions, text_other; + bool date_dependent, highlight_date_dependent; + char date_view[9]; /* "YYYYMMDD" or "" (empty -> today) */ + bool ignore_scamin; /* host debug toggle: drop SCAMIN scale-gating so every + * feature shows in-band (the *_scamin layers become a + * single ungated layer). NOT an S-52 setting. Default false. */ + double size_scale; /* physical-scale multiplier applied to icon-size / + * line-width / text-size (@2x, physical calibration). + * NOT an S-52 setting. 1.0 = catalogue sizes verbatim. */ + const int32_t *viewing_groups_off; /* S-52 §14.5 fine-grained viewing-group control: + * a DENY-LIST of the raw `vg` ids the mariner turned OFF. The + * pointee must outlive the call it is passed to. NULL/len 0 -> + * every viewing group shown. */ + uint32_t viewing_groups_off_len; + bool scamin_filter_gate; /* gate SCAMIN with a live client-driven filter instead of + * per-value bucket layers — one *_scamin layer per render-type + * (no minzoom buckets); the client rewrites the SCAMIN clause on + * boundary crossings. NOT an S-52 setting. Default false. */ + bool show_overscale; /* S-52 §10.1.10 overscale indication: the AP(OVERSC01) + * vertical-line hatch over regions displayed finer than their + * compilation scale (drives the `overscale` layer's + * visibility). Default true. */ +} tile57_mariner; + +/* Fill *m with the canonical default mariner settings (so a host needn't + * hardcode them). date_view is set to "" (today). */ +void tile57_mariner_defaults(tile57_mariner *m); + +/* ---- view renders ---------------------------------------------------------- + * + * All four render the SAME scene for a VIEW — centre (lon, lat) + fractional + * zoom + pixel size — and differ only in what they emit. `m` NULL = canonical + * defaults. width/height must be 1..16384; larger or zero is + * TILE57_ERR_BADARG. */ + +/* The view as a PNG, in *out / *out_len (free with tile57_free). */ +tile57_status tile57_chart_png(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* The view as a deterministic single-page vector PDF (1 px = 1 pt, 72 dpi; + * vector fills + native strokes + glyph-outline text), in *out / *out_len + * (free with tile57_free). */ +tile57_status tile57_chart_pdf(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* ---- callback Canvas: the pixel-space callback twin ------------------------ + * The same view painted through a table of C function pointers instead of + * rasterising to PNG — the embedder (e.g. a GPU chart app) 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 @@ -330,24 +460,19 @@ typedef struct { 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); - -/* ---- 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. */ +tile57_status tile57_chart_canvas(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + const tile57_canvas_cb *canvas, tile57_error *err); + +/* ---- world-space Surface callback: the GPU vector twin --------------------- + * The same view emitted as 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. */ 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 */ @@ -388,7 +513,7 @@ typedef struct { * (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). */ + * via draw_symbol instead. (ABI-appended after the original vtable.) */ 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 @@ -402,154 +527,191 @@ typedef struct { 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. */ -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); - -/* 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; -/* `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 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]}, ...] - * `name` is the DSNM stem; `scale` is DSPM CSCL; edition/update/issueDate/ - * agency are DSID EDTN/UPDN/ISDT/AGEN after the cell's update chain is - * applied; `bbox` is the cell's geographic extent, omitted when none parses. - * Returns 1 with *out / *out_len holding the JSON (free with tile57_free); - * 0 if the chart has no cells (e.g. a PMTiles chart — its bundle manifest - * carries the cell inventory); -1 on error. */ -int tile57_chart_cells(tile57_chart *chart, uint8_t **out, size_t *out_len); - -/* Decode a CATALOG.031 exchange-set catalogue (raw bytes) into a JSON array - * of its CATD entries: - * [{"file":"US5MD1MC/US5MD1MC.000","longName":"Annapolis Harbor", - * "impl":"BIN","bbox":[west,south,east,north]}, ...] - * `file` is the recorded path with separators normalised to '/'; `longName` - * is LFIL (the human chart title; empty when absent); `impl` is BIN/ASC/TXT; - * `bbox` is omitted when SLAT/WLON/NLAT/ELON are not all present (aux files). - * Not chart-scoped: the catalogue describes an exchange set, not an open - * chart. Returns 1 with *out / *out_len holding the JSON (free with - * tile57_free); 0 if the file holds no CATD records; -1 on parse error. */ -int tile57_catalog_entries(const uint8_t *catalog_031, size_t len, - uint8_t **out, size_t *out_len); - -/* The chart's features for the given object classes (comma-separated - * acronyms, e.g. "DEPARE,DRGARE") as a GeoJSON FeatureCollection: geometry - * in lon/lat (Polygon rings largest-first, MultiPoint with depths for - * soundings, LineString/Point as encoded), properties = {"class":"DEPARE", - * ...the feature's full S-57 acronym->value attribute map}. Parsed without - * portrayal; a whole-ENC_ROOT query walks every cell — the caller owns that - * cost. Returns 1 with *out / *out_len holding the JSON (free with - * tile57_free); 0 if no features matched; -1 on error. */ -int tile57_chart_features(tile57_chart *chart, const char *classes, - uint8_t **out, size_t *out_len); +tile57_status tile57_chart_surface(tile57_chart *chart, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + const tile57_surface_cb *surface, tile57_error *err); + +/* Portray ONE tile (z, x, y) through the SAME S-52 portrayal and the SAME + * tile57_surface_cb, but for a single tile instead of a whole view. Lets a host + * portray + tessellate each tile ONCE, cache the geometry keyed by (chart, z, x, y), + * and compose the view from cached tiles (re-portray only newly-visible tiles) — + * the MapLibre tile model, reusing tile57's portrayal. World coordinates and SCAMIN + * tags are identical to tile57_chart_surface, so the same callbacks/shaders apply. + * Decluttering is PER-TILE (labels resolve within the tile); a host wanting + * cross-tile label suppression keeps a separate view-level text pass. */ +tile57_status tile57_chart_tile_surface(tile57_chart *chart, uint8_t z, uint32_t x, uint32_t y, + const tile57_mariner *m, + const tile57_surface_cb *surface, tile57_error *err); + +/* Release a chart and all cached tiles. Must not be called while any borrower + * (a compositor, a renderer thread) may still read from it. */ +void tile57_chart_close(tile57_chart *chart); -/* Free ANY buffer the engine returned (tiles, style JSON, the scamin array, - * colortables, atlases, …), passing the same length. The universal free. */ -void tile57_free(void *ptr, size_t len); +/* ======================================================================== * + * 5. Compose + * + * The runtime compositor: MANY open charts in, one seamless chart out. It + * builds (or loads) the ownership partition over the charts' embedded + * coverage, then offers the SAME output set as a single chart, composed: + * any (z, x, y) tile on demand for the cost of a classify plus one decompress + * or one decode/clip (what a live tile server hands its HTTP layer), plus the + * composed view outputs (png / pdf / canvas / surface) and the composed cursor + * pick (query). Open once, serve many, close. + * + * The compositor BORROWS its charts (their mmap'd archives and decoded + * coverage): every chart must outlive the compositor, and while it serves, do + * not call those charts' own methods from other threads. + * ======================================================================== */ -/* Drop the in-memory tile cache to bound memory in long-running hosts. Safe to - * call any time; subsequent tile57_chart_tile calls simply regenerate/decode. */ -void tile57_chart_clear_cache(tile57_chart *chart); +/* Opaque runtime-compositor handle. */ +typedef struct tile57_compose tile57_compose; -/* ---- chart-style generation --------------------------------------------- +/* Coverage/zoom summary filled by tile57_compose_get_meta. */ +typedef struct { + uint8_t min_zoom; + uint8_t max_zoom; /* deepest zoom served (native windows + one fill-up overscale zoom) */ + uint32_t charts; /* coverage-carrying charts held */ + double west, south, east, north; /* union coverage bounds, degrees */ +} tile57_compose_meta; + +/* Open a compositor over `n` open charts (each a per-chart archive from the + * bake, opened with tile57_chart_open). Charts whose archives embed no coverage are + * skipped — they can own no ground; if none carries coverage the open is + * TILE57_ERR_UNSUPPORTED. `partition_path` (NULL to skip) names a partition + * sidecar — written by tile57_compose_save_partition (the `tile57 bake` CLI + * emits one as partition.tpart) — to load and skip the build; a missing/stale + * one falls back to building. TILE57_OK with *out set (close with + * tile57_compose_close — BEFORE closing the charts). */ +tile57_status tile57_compose_open(tile57_chart *const *charts, size_t n, + const char *partition_path, + tile57_compose **out, tile57_error *err); + +/* Compose the tile (z,x,y) on demand into RAW (decompressed) MLT in *out / + * *out_len (free with tile57_free) — the HTTP layer gzips on the wire. NULL/0 + * out with TILE57_OK means no bytes for this tile; *out_owned (NULL to ignore) + * then distinguishes the two empties: + * owned = false: no chart owns this ground — true empty ocean, safe to cache. + * owned = true: a chart owns this ground but produced nothing — transient + * while its per-chart bake is still running; suspect once + * bakes are done. */ +tile57_status tile57_compose_tile(tile57_compose *c, uint8_t z, uint32_t x, uint32_t y, + uint8_t **out, size_t *out_len, bool *out_owned, + tile57_error *err); + +/* The composed view outputs — tile57_chart_png / tile57_chart_pdf / tile57_chart_canvas / + * tile57_chart_surface across the WHOLE composed set: every covering tile is + * composed on demand (stitched through the ownership partition) and + * replayed through the native S-52 pixel path. Same parameters, limits, and + * ownership as the single-chart forms in section 4. */ +tile57_status tile57_compose_png(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); +tile57_status tile57_compose_pdf(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + uint8_t **out, size_t *out_len, tile57_error *err); +tile57_status tile57_compose_canvas(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + const tile57_canvas_cb *canvas, tile57_error *err); +tile57_status tile57_compose_surface(tile57_compose *c, double lon, double lat, double zoom, + uint32_t width, uint32_t height, + const tile57_mariner *m, + const tile57_surface_cb *surface, tile57_error *err); + +/* The composed cursor pick (S-52 §10.8, across chart boundaries): tile57_chart_query across + * the whole composed set. */ +tile57_status tile57_compose_query(tile57_compose *c, double lon, double lat, double zoom, + const tile57_query_cb *cb, tile57_error *err); + +/* Fill *out with the compositor's zoom range + union coverage bounds. */ +void tile57_compose_get_meta(tile57_compose *c, tile57_compose_meta *out); + +/* Serialize the compositor's ownership partition to the file `path` (a sidecar + * a later tile57_compose_open can load to skip the build). */ +tile57_status tile57_compose_save_partition(tile57_compose *c, const char *path, + tile57_error *err); + +/* Release a compositor. Its charts stay open (and stay yours to close). */ +void tile57_compose_close(tile57_compose *c); + +/* ======================================================================== * + * 6. Style + portrayal assets * - * tile57 ships tile generation AND style generation together: tile57_build_style + * tile57 ships tile generation AND style generation together. tile57_style_build * turns a MapLibre style template + the mariner's S-52 display options + the S-52 - * colortables into a concrete style JSON, client-side. It patches the mariner- - * driven parts of the template (depth shading, sounding/danger symbol swaps, - * contour-label units, the per-scheme recolour) and AND-s the display filters - * (category, band, boundary/point style, date validity, text groups, …) onto - * every source:"chart" layer. The template + colortables are produced by the - * engine's asset generator; the host fills tile57_mariner from its UI. */ + * colortables into a concrete style JSON, client-side; tile57_bake_assets produces + * the colour tables, line styles, and sprite / pattern / glyph atlases the style + * references. + * ======================================================================== */ -typedef enum { TILE57_SCHEME_DAY=0, TILE57_SCHEME_DUSK=1, TILE57_SCHEME_NIGHT=2 } tile57_scheme; -typedef enum { TILE57_DEPTH_METERS=0, TILE57_DEPTH_FEET=1 } tile57_depth_unit; -typedef enum { TILE57_BOUNDARY_SYMBOLIZED=0, TILE57_BOUNDARY_PLAIN=1 } tile57_boundary_style; +/* ---- portrayal assets ---------------------------------------------------- */ -typedef struct tile57_mariner { - tile57_scheme scheme; - double shallow_contour, safety_contour, deep_contour, safety_depth; - bool four_shade_water; - tile57_depth_unit depth_unit; - bool display_base, display_standard, display_other; - bool data_quality, show_inform_callouts, show_meta_bounds, show_isolated_dangers_shallow; - tile57_boundary_style boundary_style; - bool simplified_points, show_full_sector_lines; - bool text_names, show_light_descriptions, text_other; - bool date_dependent, highlight_date_dependent; - char date_view[9]; /* "YYYYMMDD" or "" (empty -> today) */ - bool ignore_scamin; /* host ?ignoreScamin debug toggle: drop SCAMIN scale-gating - * so every feature shows in-band (the *_scamin layers become - * a single ungated layer). NOT an S-52 setting. Default false. */ - double size_scale; /* physical-scale multiplier (host _featureSizeScale) applied to - * icon-size / line-width / text-size. NOT an S-52 setting. - * 1.0 = catalogue sizes verbatim. tile57_mariner_defaults sets 1.0. */ - const int32_t *viewing_groups_off; /* S-52 §14.5 fine-grained viewing-group control: - * a DENY-LIST of the raw `vg` ids the mariner turned OFF. The - * pointee must outlive the tile57_build_style call. NULL/len 0 -> - * every viewing group shown. tile57_mariner_defaults sets NULL/0. */ - uint32_t viewing_groups_off_len; - bool scamin_filter_gate; /* scamin-layers.md: gate SCAMIN with a live client-driven - * filter instead of per-value bucket layers — one *_scamin layer - * per render-type (no minzoom buckets). The client rewrites the - * SCAMIN clause on boundary crossings. NOT an S-52 setting. - * tile57_mariner_defaults sets false (per-value buckets). */ - bool show_overscale; /* S-52 §10.1.10 overscale indication: the AP(OVERSC01) - * vertical-line hatch over regions displayed finer than their - * compilation scale (drives the `overscale` layer's visibility). - * tile57_mariner_defaults sets true. */ -} tile57_mariner; +/* All portrayal assets in memory, from the library's embedded catalogue + * (catalog_dir NULL/"") or an on-disk one. Pairs with tile57_style_build + the + * composed tiles for a complete renderable chart. Free with + * tile57_assets_free. */ +typedef struct { + uint8_t *colortables; size_t colortables_len; + uint8_t *linestyles; size_t linestyles_len; + uint8_t *sprite_json; size_t sprite_json_len; uint8_t *sprite_png; size_t sprite_png_len; + uint8_t *pattern_json; size_t pattern_json_len; uint8_t *pattern_png; size_t pattern_png_len; +} tile57_assets; +tile57_status tile57_bake_assets(const char *catalog_dir, tile57_assets *out, + tile57_error *err); +/* 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. */ +tile57_status tile57_bake_sprite_mln(const char *catalog_dir, tile57_assets *out, + tile57_error *err); +/* 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. */ +tile57_status tile57_bake_glyph_sdf(tile57_assets *out, tile57_error *err); +void tile57_assets_free(tile57_assets *out); + +/* ---- chart-style generation --------------------------------------------- + * + * tile57_style_build patches the mariner-driven parts of the template (depth + * shading, sounding/danger symbol swaps, contour-label units, the per-scheme + * recolour) and AND-s the display filters (category, band, boundary/point style, + * date validity, text groups, …) onto every source:"chart" layer. The template + + * colortables are produced by the engine's asset generator; the host fills + * tile57_mariner (section 4) from its UI. + * + * The S-52 colortables and base style template are baked into the library, so a + * host can generate a complete style with no on-disk catalogue or template file: + * tile57_colortables_default(&ct,&ctn,NULL); + * tile57_style_template(scheme, "http://host/{z}/{x}/{y}", NULL,NULL,0,0,0, &t,&tn,NULL); + * tile57_style_build(t,tn, &m, ct,ctn, bands,nb, scamin,nsm,lat, &style,&sn,NULL); + * Free each buffer with tile57_free. */ /* Build a MapLibre style JSON from a template + mariner settings + S-52 colortables. * enabled_bands: NULL = no band filter (show all); else only features whose `band` * rank is in the array (count entries) are shown. * scamin: the distinct SCAMIN denominators present in the source (e.g. from - * tile57_chart_scamin / the TileJSON). When non-NULL with scamin_count>0 the - * `_scamin` source-layers are split into one per-value bucket layer with a native - * fractional minzoom = scaminDisplayZoom(value, scamin_lat) — the SAME gating the - * offline bundle style emits. NULL / count 0 -> the `_scamin` layers stay a single - * ungated layer (features render, but SCAMIN does not gate by value). + * tile57_chart_scamin / the TileJSON). When non-NULL with scamin_count>0 the `_scamin` + * source-layers are split into one per-value bucket layer with a native + * fractional minzoom = scaminDisplayZoom(value, scamin_lat). NULL / count 0 -> the + * `_scamin` layers stay a single ungated layer (features render, but SCAMIN does + * not gate by value). * scamin_lat: representative latitude (degrees) for the bucket minzooms (the SCAMIN * display cutoff is latitude-dependent); use the source's center latitude. - * On success returns 1 with the style JSON in *out / *out_len (free with - * tile57_free); 0 on error. */ -int tile57_build_style(const char *template_json, size_t template_len, - const tile57_mariner *m, - const char *colortables_json, size_t colortables_len, - const int32_t *enabled_bands, size_t enabled_band_count, - const int32_t *scamin, size_t scamin_count, double scamin_lat, - uint8_t **out, size_t *out_len); + * TILE57_OK with the style JSON in *out / *out_len (free with tile57_free). */ +tile57_status tile57_style_build(const char *template_json, size_t template_len, + const tile57_mariner *m, + const char *colortables_json, size_t colortables_len, + const int32_t *enabled_bands, size_t enabled_band_count, + const int32_t *scamin, size_t scamin_count, double scamin_lat, + uint8_t **out, size_t *out_len, tile57_error *err); /* Compute the minimal MapLibre style-mutation ops to turn the style for `old_m` * into the style for `new_m` — same template/colortables/bands/scamin inputs as - * tile57_build_style, so the two styles are comparable. For a flicker-free mariner + * tile57_style_build, so the two styles are comparable. For a flicker-free mariner * toggle the host applies each op in place (map.setFilter / setPaintProperty / * setLayoutProperty) instead of re-setStyle-ing, leaving overlays and sources * untouched. The output is a JSON array; each element is one mutation: @@ -559,30 +721,25 @@ int tile57_build_style(const char *template_json, size_t template_len, * Only layers whose filter / a paint prop / a layout prop differ appear; an * unchanged toggle yields "[]". If the two mariners would produce a DIFFERENT SET * of layers (not expected for any current mariner field — a safety valve), the - * result is [{"op":"rebuild"}], signalling the host to fall back to a full setStyle. - * On success returns 1 with the op array in *out / *out_len (free with - * tile57_free); 0 on error. */ -int tile57_style_diff(const char *template_json, size_t template_len, - const tile57_mariner *old_m, const tile57_mariner *new_m, - const char *colortables_json, size_t colortables_len, - const int32_t *enabled_bands, size_t enabled_band_count, - const int32_t *scamin, size_t scamin_count, double scamin_lat, - uint8_t **out, size_t *out_len); - -/* The S-52 colortables and base style template are baked into the library, so a - * host can generate a complete style with no on-disk catalogue or template file: - * tile57_colortables_default(&ct,&ctn); - * tile57_style_template(scheme, "http://host/{z}/{x}/{y}", NULL,NULL,0,0,0, &t,&tn); - * tile57_build_style(t,tn, &m, ct,ctn, bands,nb, scamin,nsm,lat, &style,&sn); - * Free each buffer with tile57_free. */ + * result is [{"op":"rebuild"}], signalling the host to fall back to a full + * setStyle. TILE57_OK with the op array in *out / *out_len (free with + * tile57_free). */ +tile57_status tile57_style_diff(const char *template_json, size_t template_len, + const tile57_mariner *old_m, const tile57_mariner *new_m, + const char *colortables_json, size_t colortables_len, + const int32_t *enabled_bands, size_t enabled_band_count, + const int32_t *scamin, size_t scamin_count, double scamin_lat, + uint8_t **out, size_t *out_len, tile57_error *err); /* S-52 colortables.json (token -> hex per day/dusk/night) from the colour profile - * baked into the library. Returns 1 with out/out_len set, 0 on error. */ -int tile57_colortables_default(uint8_t **out, size_t *out_len); + * baked into the library. TILE57_OK with *out / *out_len set (free with + * tile57_free). */ +tile57_status tile57_colortables_default(uint8_t **out, size_t *out_len, + tile57_error *err); /* Base MapLibre style template (layers + chart `sources` + sprite/glyph URLs) from * the catalogue baked into the library — no template file needed. The source lives - * in the template; the per-change mariner patch (tile57_build_style) takes none. + * in the template; the per-change mariner patch (tile57_style_build) takes none. * scheme: a tile57_scheme (selects the per-scheme palette). * source_tiles: the chart {z}/{x}/{y} tiles URL (NULL -> a default pmtiles:// source). * sprite,glyphs:base URLs that enable the symbol / text layers (NULL omits them). @@ -591,20 +748,33 @@ int tile57_colortables_default(uint8_t **out, size_t *out_len); * a source's minzoom, so an inflated floor blanks every lower zoom). * maxzoom: 0 -> engine default. * tile_encoding: the chart source's tile encoding (a tile57_tile_type, from - * chart_info.tile_type / the archive header). TILE57_TILE_TYPE_MLT - * emits "encoding":"mlt" on the source so maplibre-gl >= 5.12 decodes - * MLT natively; 0 / TILE57_TILE_TYPE_MVT emits nothing (the MapLibre - * default). The hint survives tile57_build_style / tile57_style_diff. - * Returns 1 with out/out_len set (free with tile57_free), 0 on error. */ -int tile57_style_template(tile57_scheme scheme, const char *source_tiles, - const char *sprite, const char *glyphs, - uint32_t minzoom, uint32_t maxzoom, - uint8_t tile_encoding, - uint8_t **out, size_t *out_len); - -/* Fill *m with the canonical default mariner settings (so a host needn't hardcode - * them). date_view is set to "" (today). */ -void tile57_mariner_defaults(tile57_mariner *m); + * tile57_info.tile_type). TILE57_TILE_TYPE_MLT emits "encoding":"mlt" on + * the source so maplibre-gl >= 5.12 decodes MLT natively; 0 / + * TILE57_TILE_TYPE_MVT emits nothing (the MapLibre default). The hint + * survives tile57_style_build / tile57_style_diff. + * TILE57_OK with *out / *out_len set (free with tile57_free). */ +tile57_status tile57_style_template(tile57_scheme scheme, const char *source_tiles, + const char *sprite, const char *glyphs, + uint32_t minzoom, uint32_t maxzoom, + uint8_t tile_encoding, + uint8_t **out, size_t *out_len, tile57_error *err); + +/* ======================================================================== * + * 7. Util + * ======================================================================== */ + +/* 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 charts 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); + +/* Free ANY buffer the engine returned (tiles, style JSON, the scamin array, + * colortables, …). Buffers are length-prefixed at allocation, so the pointer is + * all it needs — the universal free. */ +void tile57_free(void *ptr); #ifdef __cplusplus } diff --git a/src/bake_root.zig b/src/bake_root.zig index 8763975..fe29765 100644 --- a/src/bake_root.zig +++ b/src/bake_root.zig @@ -23,9 +23,11 @@ pub const tile = root.tile; pub const iso8211 = root.iso8211; pub const s57 = root.s57; pub const scene = root.scene; -pub const s101_instr = root.s101_instr; -pub const s101_adapt = root.s101_adapt; +pub const s101 = root.s101; +pub const s101_instructions = root.s101_instructions; +pub const s101_adapter = root.s101_adapter; pub const catalogue = root.catalogue; pub const bake_enc = root.bake_enc; +pub const geometry = @import("geometry"); // integer geometry: boolean, plane, partition pub const portray = @import("portray"); diff --git a/src/bundle.zig b/src/bundle.zig index 5505d23..e68180d 100644 --- a/src/bundle.zig +++ b/src/bundle.zig @@ -1,10 +1,10 @@ -//! bundle — the chart-bundle pipeline as a LIBRARY module: S-101 portrayal asset -//! emission (colortables / linestyles / sprite / patterns / sprite-mln) built from -//! the embedded catalogue (or an on-disk PortrayalCatalog dir). Moved out of the -//! `tile57` CLI so the CLI is a thin wrapper and any consumer (the C ABI, a Go/JS -//! binding, another UI) can build the same bundle. The tile-bake half (bakeRoot) + -//! the full `bakeBundle` orchestration land here next; this file currently holds the -//! asset emitters. Pure-Zig + libc (the sprite atlas builder needs nanosvg/stb). +//! bundle — S-101 portrayal asset emission as a LIBRARY module: colortables, +//! linestyles, sprite / pattern / sprite-mln atlases, built from the embedded +//! catalogue (or an on-disk PortrayalCatalog dir). Any consumer (the C ABI, a +//! Go/JS binding, another UI) builds the same assets. Also holds the ownership- +//! partition debug bake (`bakePartitionDebug`), which reuses the compositor's +//! partition-input helpers. Pure-Zig + libc (the sprite atlas builder needs +//! nanosvg/stb). The runtime compositor itself lives in the `compose` module. //! //! Each reader takes a `catalog_dir`: "" = the embedded catalogue (catalog module's //! @embedFile'd registries), else `/{Symbols,LineStyles,AreaFills, @@ -14,8 +14,9 @@ const std = @import("std"); const engine = @import("engine"); -const assets = @import("assets"); +const style = @import("style"); const sprite = @import("sprite"); +const compose = @import("compose"); // the runtime compositor (LoadedCov/toPlaneCells reused by the debug bake) const embedded_assets = @import("catalog"); // S-101 portrayal assets embedded into the binary // PortrayalCatalog sub-paths (relative to an on-disk catalog dir). @@ -42,21 +43,21 @@ fn embeddedFills(a: std.mem.Allocator) ![]sprite.AreaFillSrc { } // Embedded LineStyles/*.xml as line-style sources (id = file stem). -pub fn embeddedLinestyles(a: std.mem.Allocator) ![]assets.LineStyleSrc { - const out = try a.alloc(assets.LineStyleSrc, embedded_assets.linestyles.len); +pub fn embeddedLinestyles(a: std.mem.Allocator) ![]style.LineStyleSrc { + const out = try a.alloc(style.LineStyleSrc, embedded_assets.linestyles.len); for (embedded_assets.linestyles, 0..) |e, i| out[i] = .{ .id = e.name, .xml = e.bytes }; return out; } -// Build the complex-line tessellation table (id -> LsInfo) from `srcs` and register +// Build the complex-line tessellation table (id -> linestyle.Info) from `srcs` and register // it with scene before baking, so a named (symbolised) linestyle is drawn as its // real dash runs + embedded symbols instead of a generic dashed stroke. Mirrors Go // lsInfoFromCatalog; mm geometry is converted at the PresLib feature scale. Best // effort — a malformed/short style is simply skipped. `gpa` outlives the bake. -pub fn registerLinestyles(gpa: std.mem.Allocator, srcs: []const assets.LineStyleSrc) void { - const px = engine.scene.LINESTYLE_PX_PER_MM; +pub fn registerLinestyles(gpa: std.mem.Allocator, srcs: []const style.LineStyleSrc) void { + const px = engine.scene.linestyle.LINESTYLE_PX_PER_MM; for (srcs) |s| { - const parsed = assets.parseLineStyle(gpa, s.xml) catch continue; + const parsed = style.parseLineStyle(gpa, s.xml) catch continue; const period = parsed.interval_length * px; if (period < 0.5) continue; // no interval to tile (pure-symbol style) var runs = std.ArrayList([2]f64).empty; @@ -65,11 +66,11 @@ pub fn registerLinestyles(gpa: std.mem.Allocator, srcs: []const assets.LineStyle const hi = (d.start + d.length) * px; if (hi - lo > 1e-6) runs.append(gpa, .{ lo, hi }) catch {}; } - var syms = std.ArrayList(engine.scene.LsSym).empty; + var syms = std.ArrayList(engine.scene.linestyle.Symbol).empty; for (parsed.symbols) |sym| syms.append(gpa, .{ .name = sym.reference, .offset_px = sym.position * px }) catch {}; var width = parsed.pen_width * px; if (width < 0.6) width = 0.9; // S-52 minimum pen - engine.scene.registerLinestyle(gpa, s.id, .{ + engine.scene.linestyle.registerLinestyle(gpa, s.id, .{ .period_px = period, .on_runs = runs.items, .symbols = syms.items, @@ -112,7 +113,7 @@ pub fn colorTablesBytes(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u const xml_path = try std.fs.path.join(a, &.{ catalog_dir, COLOR_PROFILE_REL }); break :blk try std.Io.Dir.cwd().readFileAlloc(io, xml_path, a, .unlimited); }; - return assets.colorTablesJson(a, xml); + return style.colorTablesJson(a, xml); } pub fn emitColorTables(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8, out_path: []const u8) ![]u8 { @@ -125,12 +126,12 @@ pub fn emitColorTables(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8 // patterns + placed symbols). Returns the bytes (arena owned). Shared by the // assets + bundle paths. Mirrors the Go oracle's EmitS101 linestyles step. pub fn linestylesBytes(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8) ![]u8 { - if (catalog_dir.len == 0) return assets.linestylesJson(a, try embeddedLinestyles(a)); + if (catalog_dir.len == 0) return style.linestylesJson(a, try embeddedLinestyles(a)); const ls_dir_path = try std.fs.path.join(a, &.{ catalog_dir, LINESTYLES_REL }); var dir = try std.Io.Dir.cwd().openDir(io, ls_dir_path, .{ .iterate = true }); defer dir.close(io); - var srcs = std.ArrayList(assets.LineStyleSrc).empty; + var srcs = std.ArrayList(style.LineStyleSrc).empty; var walker = try dir.walk(a); defer walker.deinit(); while (try walker.next(io)) |entry| { @@ -141,7 +142,7 @@ pub fn linestylesBytes(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8 const id = std.fs.path.stem(std.fs.path.basename(path)); try srcs.append(a, .{ .id = id, .xml = xml }); } - return assets.linestylesJson(a, srcs.items); + return style.linestylesJson(a, srcs.items); } pub fn emitLinestyles(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8, out_path: []const u8) ![]u8 { @@ -241,201 +242,6 @@ pub fn emitSpriteMln(io: std.Io, a: std.mem.Allocator, catalog_dir: []const u8, return atlas; } -// ======================================================================== -// bakeBundle — the full chart-bundle orchestration (tiles + assets + manifest). -// Moved out of the `tile57 bake` CLI so any consumer (the C ABI, a Go/JS binding) -// produces the SAME self-contained bundle the CLI emits. The CLI's `runBake` is now -// a thin wrapper: parse args -> bundle.bakeBundle(opts) -> print summary. -// ======================================================================== - -// Wall-clock seconds (libc time(); std.time is behind std.Io in 0.16). For bake -// progress timing only — coarse is fine. -extern fn time(t: ?*c_long) callconv(.c) c_long; -fn nowSec() i64 { - return @intCast(time(null)); -} - -// Total parse+portray calls across the bake (atomic; workers run in parallel). -// Compared against the cell count per band to surface excessive re-parsing. -var g_parses = std.atomic.Value(usize).init(0); - -/// Inputs for `bakeBundle`. Paths are resolved by the caller: an empty `rules_dir` -/// / `catalog_dir` selects the catalogue embedded in the binary (no on-disk copy). -pub const BakeOpts = struct { - input: []const u8, // a single cell.000 OR a whole ENC_ROOT directory - out_dir: []const u8, // bundle output directory (created if absent) - rules_dir: []const u8 = "", // "" = embedded S-101 portrayal rules - catalog_dir: []const u8 = "", // "" = embedded PortrayalCatalog assets - generator: []const u8 = "", // manifest generator string (e.g. "tile57 0.1.0") - created: []const u8 = "", // manifest created stamp (ISO8601; "" = unset) - minzoom: u8 = 8, - maxzoom: u8 = 16, - lru: usize = 256, // lazy-bake tuning: parsed-GEOMETRY cells held resident. Coarse - // riders span many super-tiles, so 64 thrashed re-parses on district bakes; - // 256 holds a row of super-tiles' working set. TILE57_LRU_BUDGET overrides. - super_dz: u8 = 3, // lazy-bake tuning: spatial super-tile depth - format: engine.scene.TileFormat = .mlt, - // Cross-pack best-available: a peer pack's ENC_ROOT (same provider) read for - // M_COVR coverage only, so THIS pack's coarser cells defer where the peer is - // finer — stops an overview pack painting over the peer's sectors. "" = none. - existing_input: []const u8 = "", - // Emit per-feature pick-report attrs (s57/cell) in the baked tiles. Defaults ON; - // a lean bake sets it false to drop the bulky s57 payload. - pick_attrs: bool = true, - // Progress callback (stage 0 = loading cells, 1 = baking tiles). null = the - // built-in console progress (stderr), matching the CLI; a host passes its own. - progress: engine.bake_enc.Progress = null, - progress_user: ?*anyopaque = null, -}; - -/// What `bakeBundle` produced: the baked cell count + the union bbox [w,s,e,n]. -pub const BakeResult = struct { cell_count: usize, bounds: [4]f64 }; - -/// Bake a single cell or a whole ENC_ROOT into a self-contained chart bundle under -/// `opts.out_dir`: tiles/chart.pmtiles + assets/{colortables,linestyles}.json + -/// sprite-mln + style-{day,dusk,night}.json + manifest.json (pins schema_version, -/// couples tiles to portrayal). Asset-emit failures degrade gracefully (a warning + -/// a thinner bundle); a structural failure (no geometry, write error) is returned. -pub fn bakeBundle(io: std.Io, a: std.mem.Allocator, opts: BakeOpts) !BakeResult { - // 1. tiles -> /tiles/chart.pmtiles. Input may be a single cell.000 or a - // whole ENC_ROOT directory (band-streamed multi-cell bake, streamed to disk). - const tiles_dir = try std.fs.path.join(a, &.{ opts.out_dir, "tiles" }); - try std.Io.Dir.cwd().createDirPath(io, tiles_dir); - const tiles_path = try std.fs.path.join(a, &.{ tiles_dir, "chart.pmtiles" }); - const rb = try bakeRoot(io, a, opts.input, tiles_path, opts.rules_dir, opts.minzoom, opts.maxzoom, opts.lru, opts.super_dz, true, opts.format, opts.pick_attrs, opts.existing_input, opts.progress, opts.progress_user); - const bounds = rb.bounds; - const cells = rb.cells; - const snd_stacks = rb.sounds; // sounding glyph stacks for sprite-mln - - // 2. assets -> /assets/colortables.json + style-{day,dusk,night}.json - const assets_dir = try std.fs.path.join(a, &.{ opts.out_dir, "assets" }); - try std.Io.Dir.cwd().createDirPath(io, assets_dir); - const ct_path = try std.fs.path.join(a, &.{ assets_dir, "colortables.json" }); - const ls_path = try std.fs.path.join(a, &.{ assets_dir, "linestyles.json" }); - _ = emitLinestyles(io, a, opts.catalog_dir, ls_path) catch |err| - std.debug.print("warning: linestyles emit failed ({s})\n", .{@errorName(err)}); - // The bundle ships ONLY the MapLibre-ready sprite (sprite-mln): it already - // contains the pivot-centred symbols + ctr:/pat: variants, so the raw - // sprite.json/patterns.json (the web/oracle format) would be redundant here — - // they stay available via the standalone `tile57 sprite`/`pattern` commands. - // If sprite-mln emits, the styles get a `sprite` URL so symbols + patterns - // render; snd_stacks (collected during the bake) gives it the sounding composites. - const sprite_ok = if (emitSpriteMln(io, a, opts.catalog_dir, DEFAULT_CSS, assets_dir, "sprite-mln", snd_stacks)) |_| true else |err| blk: { - std.debug.print("warning: sprite-mln emit failed ({s})\n", .{@errorName(err)}); - break :blk false; - }; - var styles: ?assets.Manifest.Styles = null; - if (emitColorTables(io, a, opts.catalog_dir, ct_path)) |ct| { - // One style.json per palette, resolving colour tokens from the colortables. - // sprite enables the symbol/pattern layers; glyphs (text) await SDF glyphs. - var ok = true; - // SCAMIN bucket minzooms are latitude-dependent; fix them at the archive center. - const scamin_lat = (bounds[1] + bounds[3]) / 2.0; - for ([_][]const u8{ "day", "dusk", "night" }) |sc| { - const sj = assets.styleJson(a, .{ - .scheme = sc, - .colortables_json = ct, - // The bundle style must hint the tiles' encoding so maplibre-gl - // (>=5.12) picks its MLT decoder for an MLT bake; MVT emits nothing. - .encoding = if (opts.format == .mlt) "mlt" else null, - .sprite = if (sprite_ok) "sprite-mln" else null, - .minzoom = opts.minzoom, - .scamin = rb.scamin, - .scamin_lat = scamin_lat, - }) catch { - ok = false; - break; - }; - const name = try std.fmt.allocPrint(a, "style-{s}.json", .{sc}); - const sp = try std.fs.path.join(a, &.{ assets_dir, name }); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = sp, .data = sj }); - } - if (ok) styles = .{ - .day = "assets/style-day.json", - .dusk = "assets/style-dusk.json", - .night = "assets/style-night.json", - }; - } else |err| { - std.debug.print("warning: assets emit failed ({s}); bundle has tiles + manifest only\n", .{@errorName(err)}); - } - - // 3. manifest.json — pins schema_version, couples tiles <-> portrayal. - const b = bounds; - const manifest = try assets.manifestJson(a, .{ - .generator = opts.generator, - .created = opts.created, - .tiles_rel = "tiles/chart.pmtiles", - .colortables_rel = "assets/colortables.json", - .minzoom = opts.minzoom, - .maxzoom = opts.maxzoom, - .bbox = b, - .anchor = .{ (b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0 }, - .cells = cells, - .styles = styles, - }); - const manifest_path = try std.fs.path.join(a, &.{ opts.out_dir, "manifest.json" }); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = manifest_path, .data = manifest }); - - return .{ .cell_count = cells.len, .bounds = bounds }; -} - -// ---- bake-root (whole ENC_ROOT / single cell -> one banded PMTiles) ----- - -// True if `path` is a directory (an ENC_ROOT) rather than a single cell.000 file. -fn isDir(io: std.Io, path: []const u8) bool { - var d = std.Io.Dir.cwd().openDir(io, path, .{}) catch return false; - d.close(io); - return true; -} - -// Live bake-root progress context (file-level so the C-ABI Progress callback can -// read the current band + super-tile position alongside the running tile count). -const ProgressCtx = struct { band: []const u8 = "?", st: usize = 0, st_total: usize = 0, band_start: i64 = 0 }; -var g_prog: ProgressCtx = .{}; -var g_bake_start: i64 = 0; - -/// Console progress for bake-root: stage 0 = loading cells, 1 = baking tiles. -/// Shows the band label (name + i/n), super-tile position, the per-band tile bar -/// (done/total %, host §3) and throughput. `done`/`total` for stage 1 are per-band -/// (total 0 = unknown). `band_index`/`band_count`/`band_name` are the host §3 band -/// label; band_name (when given) overrides g_prog.band so it's always consistent. -fn cliProgress(ctx: ?*anyopaque, stage: u8, done: usize, total: usize, band_index: u8, band_count: u8, band_name: ?[*:0]const u8) callconv(.c) void { - _ = ctx; - const name: []const u8 = if (band_name) |p| std.mem.span(p) else g_prog.band; - const elapsed = @as(f64, @floatFromInt(@max(nowSec() - g_bake_start, 1))); - if (stage == 0) { - std.debug.print("\r {s}: loading {d} cells ", .{ name, done }); - } else { - const band_secs = @as(f64, @floatFromInt(@max(nowSec() - g_prog.band_start, 1))); - const rate = @as(f64, @floatFromInt(done)) / band_secs; - if (total > 0) { - const pct = @as(f64, @floatFromInt(done)) * 100.0 / @as(f64, @floatFromInt(total)); - std.debug.print("\r {s} (band {d}/{d}): super-tile {d}/{d} {d}/{d} tiles ({d:.0}%) ({d:.0}/s, {d:.0}s) ", .{ name, band_index + 1, band_count, g_prog.st, g_prog.st_total, done, total, pct, rate, elapsed }); - } else { - std.debug.print("\r {s} (band {d}/{d}): super-tile {d}/{d} {d} tiles ({d:.0}/s, {d:.0}s) ", .{ name, band_index + 1, band_count, g_prog.st, g_prog.st_total, done, rate, elapsed }); - } - } -} - -// One ENC_ROOT base cell: its `.000` path (updates are .001…, found on load) -// and the cheap peek bbox [w,s,e,n], used to assign it to spatial super-tiles -// without a full parse. `light` (patched in from the scamin pre-pass, which -// parses every cell once) is the conservative attr-based sector-figure reach -// (scene.scanLightReachAttrs): the super-tile index + planned-tile estimate -// widen the cell's span by it so light legs/arcs crossing tile / super-tile -// boundaries stay addressed (and the cell is loaded for the neighbouring -// super-tiles its figures reach into). -const CellEntry = struct { path: []const u8, bbox: [4]f64, light: engine.scene.LightReach = .{} }; - -// ---- Light-reach pre-pass ----------------------------------------------- -// Before any band pass, every cell is read+parsed once (parallel, transient) -// and scanned for its attr-based sector-figure reach -// (scene.scanLightReachAttrs): the super-tile index + planned-tile spans widen -// each cell's span by it so light legs/arcs crossing tile / super-tile -// boundaries stay addressed. (The scamin_pts overlay dedup that used to ride -// this pass is gone — SCAMIN points go through the coverage-clipped composite -// like every other feature.) - // Read + parse one cell (base .000 + sequential updates) from `dir`; null on // any failure. The parsed cell allocates from smp_allocator (thread-safe). fn readParseCell(io: std.Io, dir: std.Io.Dir, bpath: []const u8) ?engine.s57.Cell { @@ -462,19 +268,27 @@ fn readParseCell(io: std.Io, dir: std.Io.Dir, bpath: []const u8) ?engine.s57.Cel return engine.s57.parseCellWithUpdates(gpa, base, ups.items) catch null; } -/// Cross-pack best-available (BakeOpts.existing_input / bake --existing): peer packs' -/// M_COVR coverage + cscl, read from a sibling ENC_ROOT for suppression context only -/// (never emitted). One cell resident at a time; coverage is copied into `a` so each -/// cell frees right after. "" / a missing dir yields no context (a plain bake). -fn loadContextCoverage(io: std.Io, a: std.mem.Allocator, path: []const u8) []engine.bake_enc.ContextCell { +/// THE eager cell-coverage loader: walk an ENC_ROOT, parse each cell once (base + +/// updates via readParseCell), and capture its M_COVR coverage + bbox + cscl + name +/// + date. One entry per stem (a boundary cell shared by two districts loads once). +/// Coverage is copied into `a` and survives the cell's deinit. "" / a missing dir +/// yields none; cells with no M_COVR are skipped. The single load path composed by +/// the cross-pack context, the partition debug bake, and the compositor. +fn loadCells(io: std.Io, a: std.mem.Allocator, path: []const u8) []compose.LoadedCov { if (path.len == 0) return &.{}; var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch return &.{}; defer dir.close(io); + var out = std.ArrayList(compose.LoadedCov).empty; + var seen = std.StringHashMap(void).init(a); var walker = dir.walk(a) catch return &.{}; defer walker.deinit(); - var out = std.ArrayList(engine.bake_enc.ContextCell).empty; while (walker.next(io) catch null) |entry| { if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".000")) continue; + const stem = std.fs.path.stem(std.fs.path.basename(entry.path)); + if (seen.contains(stem)) continue; + const stem_d = a.dupe(u8, stem) catch continue; + seen.put(stem_d, {}) catch {}; + var cell = readParseCell(io, dir, entry.path) orelse continue; defer cell.deinit(); const cov = cell.mcovrCoverage(a); // copied into `a`; survives cell.deinit() @@ -486,1005 +300,222 @@ fn loadContextCoverage(io: std.Io, a: std.mem.Allocator, path: []const u8) []eng b[2] = @max(b[2], p.lon()); b[3] = @max(b[3], p.lat()); }; - out.append(a, .{ .coverage = cov, .bounds = b, .cscl = cell.params.cscl }) catch {}; + out.append(a, .{ + .name = stem_d, + .date = a.dupe(u8, cell.dsid.isdt) catch "", + .cscl = cell.params.cscl, + .coverage = cov, + .bounds = b, + }) catch {}; } return out.toOwnedSlice(a) catch &.{}; } -const LightScanWork = struct { - entries: []const CellEntry, - dir: std.Io.Dir, - io: std.Io, - lights: []engine.scene.LightReach, - - fn run(uptr: *anyopaque, i: usize, scratch: std.mem.Allocator) void { - _ = scratch; - const c: *LightScanWork = @ptrCast(@alignCast(uptr)); - var cell = readParseCell(c.io, c.dir, c.entries[i].path) orelse return; - defer cell.deinit(); - c.lights[i] = engine.scene.scanLightReachAttrs(&cell); - } -}; - -// Scan every root cell's attr-based sector-figure reach in parallel (one -// transient parse per cell). `light_out` is `entries`-aligned; best-effort. -fn scanEntryLights(io: std.Io, dir: std.Io.Dir, entries: []const CellEntry, light_out: []engine.scene.LightReach) void { - if (entries.len == 0 or light_out.len < entries.len) return; - var lw = LightScanWork{ .entries = entries, .dir = dir, .io = io, .lights = light_out }; - engine.bake_enc.parallelFor(std.heap.smp_allocator, entries.len, &lw, LightScanWork.run); -} - -// A cell's portrayal: the per-feature S-101 instruction streams (+ display -// variants), computed ONCE by the Lua engine and kept resident for the whole band -// in `arena`. Compact relative to geometry, and the expensive thing to produce — -// so it's never recomputed, even when the cell's geometry is evicted + re-parsed. -const CellPortray = struct { - arena: *std.heap.ArenaAllocator, // sticky: owns the streams below - base: ?[]const ?[]const u8, - plain: ?[]const ?[]const u8, - simplified: ?[]const ?[]const u8, - bounds: [4]f64, - // EXACT sector-figure reach from the portrayal streams - // (scene.collectLightReach) — drives the Backend's buildTileMap reach ring - // and the LIGHTS feature-cull margin. Resident with the streams so a - // geometry re-parse doesn't recompute it. - light: engine.scene.LightReach = .{}, +// A cyclic palette so adjacent ownership faces get distinct colours in the debug +// view; a face carries its colour as a `color` property for a trivial data-driven style. +const DEBUG_PALETTE = [_][]const u8{ + "#e6194b", "#3cb44b", "#ffe119", "#4363d8", "#f58231", "#911eb4", "#46f0f0", "#f032e6", + "#bcf60c", "#fabebe", "#008080", "#e6beff", "#9a6324", "#fffac8", "#800000", "#aaffc3", }; -// A cell's parsed geometry (the heavy part): the S-57 model + assembled geo cache. -// EVICTABLE under LRU pressure and re-parsed on demand (cheap — no re-portrayal). -const CellGeom = struct { - cell: engine.s57.Cell, // cell.arena owns the geometry - geo: ?engine.scene.GeoParts, - geo_world: ?engine.scene.GeoWorld = null, // precomputed world coords (in geo_arena) - geo_arena: ?*std.heap.ArenaAllocator, - feat_bbox: []const ?[4]f64 = &.{}, // per-feature bbox for the per-tile cull (in geo_arena) - coverage: []const []const []const engine.s57.LonLat = &.{}, // M_COVR (cell.arena) for quilting - scamins: []const u32 = &.{}, // distinct SCAMIN denoms (cell.arena) — the handoff ladder -}; - -// A deferred band's cells riding into the next-coarser pass (the band handoff): -// entries + resident portrayal + surviving geometry, index-aligned. The consuming -// pass appends them behind its own cells (bakeBand own_len) so the deferred floor -// tiles bake with BOTH bands' cells — WITHOUT re-portraying (the expensive step) — -// then frees them at its end. Peak memory: two adjacent bands, as specced. -const CarryState = struct { - entries: []const CellEntry, - portray: []?CellPortray, - geom: []?CellGeom, - gave_up: []bool, - - // Fresh all-null state for entries whose own pass never loaded them (a fully - // deferred zoom clamp): the consuming pass loads + portrays them itself. - fn initEmpty(gpa: std.mem.Allocator, entries: []const CellEntry) !CarryState { - const po = try gpa.alloc(?CellPortray, entries.len); - @memset(po, null); - const ge = try gpa.alloc(?CellGeom, entries.len); - @memset(ge, null); - const gu = try gpa.alloc(bool, entries.len); - @memset(gu, false); - return .{ .entries = entries, .portray = po, .geom = ge, .gave_up = gu }; +/// Bake a DEBUG PMTiles of the ownership PARTITION (which cell renders which ground), +/// NO portrayed content, for eyeballing the composite quilt. `band` < 0 emits the band +/// GOVERNING each zoom (the natural view); 0..5 (berthing..overview) emits only that +/// band's own map, at every zoom. Composes the single paths — loadCells + compose.toPlaneCells +/// + geometry.plane partition + the mvt/tile/pmtiles primitives — and STREAMS tiles (one +/// tier + one zoom resident) so it scales to the whole corpus. Returns the cell count; +/// `out_path` is a single `.pmtiles` file. +pub fn bakePartitionDebug(io: std.Io, gpa: std.mem.Allocator, root_path: []const u8, out_path: []const u8, minzoom: u8, maxzoom_arg: u8, band: i8) !usize { + const geometry = engine.geometry; + const maxzoom = @min(maxzoom_arg, @as(u8, 16)); + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + + // Single load + adapt paths. Reach-cap each cell exactly as the compositor does + // (band terms only — a native per-cell bake never exceeds its band window), so the + // debug quilt shows the partition the compositor actually composes with, not a + // hypothetical full pool. + const loaded = loadCells(io, a, root_path); + if (loaded.len == 0) return error.NoGeometry; + const cells = try compose.toPlaneCells(a, loaded); + for (cells) |*c| c.reach = compose.bandReach(c.cscl); + + // Union bbox (E7) for the header, so a viewer opens on the data not the world. + var ubox = [4]i32{ std.math.maxInt(i32), std.math.maxInt(i32), std.math.minInt(i32), std.math.minInt(i32) }; + for (loaded) |c| { + ubox[0] = @min(ubox[0], deg7(c.bounds[0])); + ubox[1] = @min(ubox[1], deg7(c.bounds[1])); + ubox[2] = @max(ubox[2], deg7(c.bounds[2])); + ubox[3] = @max(ubox[3], deg7(c.bounds[3])); } + std.debug.print("partition-debug: {d} cells from {s}, z{d}-{d}, band {d}\n", .{ loaded.len, root_path, minzoom, maxzoom, band }); - // Free the surviving cells + portrayal arenas + the slices themselves. - fn deinit(self: *const CarryState, gpa: std.mem.Allocator) void { - for (self.geom) |*g| if (g.*) |*gg| { - gg.cell.deinit(); - if (gg.geo_arena) |ga| { - ga.deinit(); - gpa.destroy(ga); - } - }; - // Portrayal arenas are owned by the bake-lifetime pcache; free only the slice. - gpa.free(self.portray); - gpa.free(self.geom); - gpa.free(self.gave_up); - } -}; - -// Copy per-feature instruction streams into `a` (the sticky portrayal arena) so -// they outlive the transient portrayal arena. nulls (features with no stream) stay. -fn copyStreams(a: std.mem.Allocator, src: []const ?[]const u8) ![]const ?[]const u8 { - const out = try a.alloc(?[]const u8, src.len); - for (src, 0..) |s, i| out[i] = if (s) |bytes| try a.dupe(u8, bytes) else null; - return out; -} - -// Parallel cell loader. Each index READS its cell's .000 (+ sequential updates) -// from disk, parses it (a fresh per-thread Lua state; page allocator is -// thread-safe), builds its geo cache (evictable), and — only the first time the -// cell is loaded (portray[ci] == null) — runs the S-101 portrayal and copies the -// streams into a sticky arena. Reading in the worker (std.Io.Dir.readFileAlloc is -// thread-safe: each call opens its own handle, no shared mutable state) makes a -// super-tile's cell reads parallel, so a cold first-bake isn't disk-read-serial. -// Re-loads after a geometry eviction re-parse only, reusing the resident -// portrayal. Distinct ci per j ⇒ race-free. -const LoadWork = struct { - cells: []const u32, // cell indices into `entries` - entries: []const CellEntry, // cell file path; bytes read in-worker (parallel I/O) - dir: std.Io.Dir, - io: std.Io, - portray: []?CellPortray, // resident; produced only when null - geom: []?CellGeom, // (re)built every load - rules_dir: []const u8, - gpa: std.mem.Allocator, - - fn run(uptr: *anyopaque, j: usize, scratch: std.mem.Allocator) void { - _ = scratch; // parsed cell + portrayal are persistent; kept in their own arenas - const c: *LoadWork = @ptrCast(@alignCast(uptr)); - const ci = c.cells[j]; - const bpath = c.entries[ci].path; - const base = c.dir.readFileAlloc(c.io, bpath, c.gpa, .unlimited) catch return; - defer c.gpa.free(base); - if (base.len == 0) return; - // Read sequential updates (.001…) until the first missing one. - var ups = std.ArrayList([]const u8).empty; - defer { - for (ups.items) |ub| c.gpa.free(ub); - ups.deinit(c.gpa); - } - const stem = bpath[0 .. bpath.len - 4]; - var u: u32 = 1; - while (u <= 999) : (u += 1) { - const upn = std.fmt.allocPrint(c.gpa, "{s}.{d:0>3}", .{ stem, u }) catch break; - defer c.gpa.free(upn); - const ub = c.dir.readFileAlloc(c.io, upn, c.gpa, .unlimited) catch break; - ups.append(c.gpa, ub) catch { - c.gpa.free(ub); - break; - }; - } - _ = g_parses.fetchAdd(1, .monotonic); // count parses (re-parse diagnostics) - var cell = engine.s57.parseCellWithUpdates(c.gpa, base, ups.items) catch return; - cell.name = std.fs.path.stem(std.fs.path.basename(bpath)); // pick-report source-cell badge (slice into the bake-lived entry path) - const b = cell.bounds() orelse { - cell.deinit(); - return; - }; - var geo: ?engine.scene.GeoParts = null; - var geo_world: ?engine.scene.GeoWorld = null; - var geo_arena: ?*std.heap.ArenaAllocator = null; - var feat_bbox: []const ?[4]f64 = &.{}; - if (c.gpa.create(std.heap.ArenaAllocator)) |ga| { - ga.* = std.heap.ArenaAllocator.init(c.gpa); - // Assemble line/area geometry once + its world coords, so every tile - // reprojects cheaply (no per-point tan/log) — the bake's biggest cost. - if (engine.scene.buildGeoCache(ga.allocator(), &cell)) |g| { - geo = g; - geo_world = engine.scene.buildGeoWorld(ga.allocator(), g) catch null; - } else |_| {} - feat_bbox = engine.scene.buildFeatBBox(ga.allocator(), &cell, geo) catch &.{}; - geo_arena = ga; - } else |_| {} - const coverage = cell.mcovrCoverage(cell.arena.allocator()); // before the move (cell.arena-owned) - // Distinct SCAMIN denominators (the band-handoff quantization ladder) — - // cheap feature scan, rebuilt with the cell on every re-parse. - const scamins = engine.bake_enc.collectScamins(cell.arena.allocator(), &cell) catch &.{}; - c.geom[ci] = .{ .cell = cell, .geo = geo, .geo_world = geo_world, .geo_arena = geo_arena, .feat_bbox = feat_bbox, .coverage = coverage, .scamins = scamins }; - - // Label-point cache: built from the (reused or fresh) portrayal — only features - // whose base stream draws a Text/centred-symbol consult labelPoint, so cache just - // those (see scene.buildLabelCache). Set on the STORED cell so the per-tile emit - // reuses it instead of re-running the dominant polylabel search every tile. - if (c.portray[ci]) |pc| { // already portrayed — reuse it (the speed win) - if (c.geom[ci]) |*g| if (g.geo_arena) |ga| { - g.cell.label_cache = engine.scene.buildLabelCache(ga.allocator(), &g.cell, g.geo, pc.base) catch null; - }; - return; - } - const sticky = c.gpa.create(std.heap.ArenaAllocator) catch return; - sticky.* = std.heap.ArenaAllocator.init(c.gpa); - const sa = sticky.allocator(); - var tmp = std.heap.ArenaAllocator.init(c.gpa); - defer tmp.deinit(); - if (engine.portray.portrayCellVariants(tmp.allocator(), &cell, c.rules_dir)) |cp| { - c.portray[ci] = .{ - .arena = sticky, - .base = copyStreams(sa, cp.base) catch null, - .plain = if (cp.plain) |p| (copyStreams(sa, p) catch null) else null, - .simplified = if (cp.simplified) |p| (copyStreams(sa, p) catch null) else null, - .bounds = b, - .light = engine.scene.collectLightReach(&cell, cp.base), - }; - } else |_| { - // Portrayal failed: a resident entry with no streams (classify() fallback); - // marks the cell portrayed so it isn't retried. - c.portray[ci] = .{ .arena = sticky, .base = null, .plain = null, .simplified = null, .bounds = b }; + // Stream tiles (gzipped + deduped) so the whole archive never lives uncompressed. + var sw = engine.pmtiles.StreamWriter.init(gpa); + defer sw.deinit(); + var zoom_arena = std.heap.ArenaAllocator.init(gpa); // one zoom's tiles at a time + defer zoom_arena.deinit(); + + if (band >= 0) { + // One band's own map: build its tier once, emit across every zoom. + const bandv: engine.bake_enc.Band = @enumFromInt(@as(u8, @intCast(@min(band, @as(i8, 5))))); + const tier = engine.bake_enc.bandZooms(bandv).min; + const faces = try geometry.plane.ownedAtTierIndexed(gpa, cells, tier); + defer geometry.plane.freeOwned(gpa, faces); + var z: u8 = minzoom; + while (z <= maxzoom) : (z += 1) { + _ = zoom_arena.reset(.retain_capacity); + try emitFacesZoom(&sw, zoom_arena.allocator(), faces, loaded, z, tier); } - if (c.geom[ci]) |*g| if (g.geo_arena) |ga| { - g.cell.label_cache = engine.scene.buildLabelCache(ga.allocator(), &g.cell, g.geo, c.portray[ci].?.base) catch null; - }; - } -}; - -// A whole-ENC_ROOT bake result: the union bbox of the cells, their stems (for the -// bundle manifest), the distinct sounding glyph stacks collected while baking (for -// sprite-mln), and the distinct SCAMIN denominators (for the client bucket manifest). -// The archive itself is streamed straight to `out_path`. -const RootBake = struct { bounds: [4]f64, cells: []const []const u8, sounds: []const []const u8, scamin: []const u32 = &.{} }; - -// Per-tile sink for the streaming bake: feed each tile into the PMTiles -// StreamWriter (gzip+dedup, no raw-tile retention) and, in the same pass, collect -// its soundings-layer glyph stacks (so sprite-mln gets composites without a -// re-read). A per-tile scratch arena keeps the MVT decode from accumulating. -const BakeSink = struct { - sw: *engine.pmtiles.StreamWriter, - sounds: *std.StringHashMap(void), - scamin: *std.AutoHashMap(u32, void), // distinct SCAMIN denominators -> archive manifest - a: std.mem.Allocator, // persistent (sound-stack strings; returned to caller) - gpa: std.mem.Allocator, // scratch backing - collect_sounds: bool, // bundle wants sprite-mln sounding composites; plain bake doesn't - format: engine.scene.TileFormat, // baked tile encoding — selects the decode codec below - - // `comp` is the already-gzipped tile (compressed in the gen worker), so the - // serial path here is just dedup + write — fast. Sounding-stack + SCAMIN-manifest - // collection (only for the bundle) gunzips + decodes with the codec matching the - // bake format (mlt.decode returns the same DecodedLayer shape as mvt.decode); - // plain bake-root skips it. - fn run(ctx: ?*anyopaque, z: u8, x: u32, y: u32, comp: []const u8) anyerror!void { - const self: *BakeSink = @ptrCast(@alignCast(ctx.?)); - if (self.collect_sounds) collect: { - var scratch = std.heap.ArenaAllocator.init(self.gpa); - defer scratch.deinit(); - const raw = engine.gzip.decompress(scratch.allocator(), comp) catch break :collect; - const layers = switch (self.format) { - .mvt => engine.mvt.decode(scratch.allocator(), raw) catch break :collect, - .mlt => engine.mlt.decode(scratch.allocator(), raw) catch break :collect, - }; - for (layers) |L| { - const is_snd = std.mem.eql(u8, L.name, "soundings"); - for (L.features) |feat| for (feat.properties) |p| { - // Distinct SCAMIN denominators across EVERY layer -> the client's - // scamin ladder (filter-gate crossings). oscl (overscale) tags - // join it identically: the AP(OVERSC01) gate must flip at a - // client crossing (specs/overscale.md). - if (std.mem.eql(u8, p.key, "scamin") or std.mem.eql(u8, p.key, "oscl")) { - switch (p.value) { - .int => |iv| if (iv > 0) self.scamin.put(@intCast(iv), {}) catch {}, - else => {}, - } - continue; - } - // Soundings glyph stacks for the sprite-mln composite atlas. Both - // unit variants must be collected: the SAME atlas serves the metres - // (sym_s/sym_g) and the feet (sym_s_ft/sym_g_ft) styles — the client - // swaps the icon-image field at runtime, so a feet composite the - // metres pass never referenced (e.g. a deep wreck/obstruction whose - // feet digits compose differently) would otherwise be missing and the - // sounding render blank in feet mode. - if (!is_snd) continue; - const k = p.key; - const is_sound_glyph = std.mem.eql(u8, k, "sym_s") or std.mem.eql(u8, k, "sym_g") or - std.mem.eql(u8, k, "sym_s_ft") or std.mem.eql(u8, k, "sym_g_ft") or - std.mem.eql(u8, k, "symbol_names"); - if (!is_sound_glyph) continue; - switch (p.value) { - .string => |sv| if (std.mem.indexOfScalar(u8, sv, ',') != null and !self.sounds.contains(sv)) { - self.sounds.put(self.a.dupe(u8, sv) catch return, {}) catch {}; - }, - else => {}, - } - }; + } else { + // Merged: the band governing each zoom. Build that tier lazily, one resident at + // a time (coarse→fine as z rises), so the finest tier is only built if reached. + const floors = try distinctFloorsDesc(a, cells); + var cur_tier: i16 = -1; + var cur_faces: []geometry.plane.OwnedCell = &.{}; + defer if (cur_tier >= 0) geometry.plane.freeOwned(gpa, cur_faces); + var z: u8 = minzoom; + while (z <= maxzoom) : (z += 1) { + const t = governingTier(floors, z); + if (cur_tier != @as(i16, t)) { + if (cur_tier >= 0) geometry.plane.freeOwned(gpa, cur_faces); + cur_faces = try geometry.plane.ownedAtTierIndexed(gpa, cells, t); + cur_tier = t; } + _ = zoom_arena.reset(.retain_capacity); + try emitFacesZoom(&sw, zoom_arena.allocator(), cur_faces, loaded, z, t); } - try self.sw.addCompressed(z, x, y, comp); } -}; -// Parse the named environment variable as a usize, returning `dflt` when it is -// unset or unparseable — the bake-tuning host toggle (TILE57_SUPER_DZ / -// TILE57_LRU_BUDGET). Reads the process environment the host launched with; no -// ABI/flag surface needed. The engine is linked into the host, so this sees the -// host's env. -fn envUsizeOr(name: [:0]const u8, dflt: usize) usize { - const v = std.c.getenv(name.ptr) orelse return dflt; - const s = std.mem.sliceTo(v, 0); - return std.fmt.parseInt(usize, std.mem.trim(u8, s, " \t\r\n"), 10) catch dflt; + // Frame a viewer on the data (a zoom where the bbox is ~2 tiles wide). + const span_deg = @max(0.01, @as(f64, @floatFromInt(ubox[2] - ubox[0])) / 1e7); + const cz: u8 = @intFromFloat(std.math.clamp(std.math.log2(720.0 / span_deg), @as(f64, @floatFromInt(minzoom)), @as(f64, @floatFromInt(maxzoom)))); + const bytes = try sw.finishBytes(.{ + // MapLibre/pmtiles.io build their style from `vector_layers`; without it the + // "partition" layer is invisible to a viewer no matter the geometry. + .metadata_json = "{\"name\":\"partition-debug\",\"format\":\"pbf\",\"vector_layers\":[{\"id\":\"partition\",\"fields\":{\"cell\":\"String\",\"cscl\":\"Number\",\"band\":\"Number\",\"tier\":\"Number\",\"oi\":\"Number\",\"color\":\"String\"}},{\"id\":\"labels\",\"fields\":{\"cell\":\"String\"}}]}", + .tile_type = .mvt, + .min_lon_e7 = ubox[0], + .min_lat_e7 = ubox[1], + .max_lon_e7 = ubox[2], + .max_lat_e7 = ubox[3], + .center_zoom = cz, + }); + defer gpa.free(bytes); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = bytes }); + return loaded.len; } -// Bake an ENC_ROOT directory (or a single cell.000) into one band-streamed PMTiles -// archive written straight to `out_path` (the data section streams through a -// StreamWriter — only the compressed data + small directory are held, not the raw -// tiles). Returns the union bounds + cell stems + sounding stacks + SCAMIN denoms. -// error.NoGeometry if no cell parses. -fn bakeRoot(io: std.Io, a: std.mem.Allocator, root_path: []const u8, out_path: []const u8, rules_dir: []const u8, minzoom: u8, maxzoom: u8, lru_budget_arg: usize, super_dz_arg: u8, collect_sounds: bool, format: engine.scene.TileFormat, pick_attrs: bool, existing_path: []const u8, progress: engine.bake_enc.Progress, progress_user: ?*anyopaque) !RootBake { - // Bake-tuning env overrides (host toggle, no ABI/flag change): TILE57_SUPER_DZ / - // TILE57_LRU_BUDGET replace the caller's defaults for the lazy super-tile bake — - // super_dz = how coarse a "super-tile" the band is loaded/generated in (bigger = - // fewer, larger super-tiles = more cells resident at once), lru_budget = the cap - // on parsed+portrayed cells kept between super-tiles. Read once per bake. - const lru_budget = envUsizeOr("TILE57_LRU_BUDGET", lru_budget_arg); - const super_dz: u8 = @intCast(@min(envUsizeOr("TILE57_SUPER_DZ", super_dz_arg), @as(usize, 16))); - // Overscale fill-up depth (crisp zooms past each band's window; 0-2). Cost - // ~4x that band's uncovered footprint per zoom; blank water never returns - // at 0 (camera cap + one-level stretch) — this dial buys crisp depth. - const fillup_dz: u8 = @intCast(@min(envUsizeOr("TILE57_FILLUP_DZ", engine.bake_enc.FILLUP_DZ), @as(usize, 2))); - // Progress sink: the caller's callback, else the built-in console writer (the CLI - // path, byte-identical to before). Both only touch stderr — never the tile bytes. - const prog: engine.bake_enc.Progress = progress orelse cliProgress; - // smp_allocator (Zig's fast thread-safe general-purpose allocator), not - // page_allocator: the bake makes many small, short-lived allocations across - // worker threads (per-tile gzip results, cell reads, index lists). page_allocator - // rounds every allocation up to a page and mmaps it, churning page faults; - // smp_allocator pools and reuses freed blocks. - const gpa = std.heap.smp_allocator; - // Bake-lifetime portrayal cache, keyed by cell path: a cell can appear in - // SEVERAL passes (its own band, a coarse-rider slot in the next-finer pass, - // a carry slot in the next-coarser) and portrayal is ~80% of the bake — so - // it runs ONCE per cell per bake, whichever pass loads it first. The cache - // owns every portrayal arena (pass-end no longer frees them; CarryState - // frees only its slices) and releases them all when the bake returns. - var pcache = std.StringHashMap(CellPortray).init(gpa); - defer { - var pit = pcache.valueIterator(); - while (pit.next()) |pc| { - pc.arena.deinit(); - gpa.destroy(pc.arena); - } - pcache.deinit(); - } - // Input may be a whole ENC_ROOT directory OR a single cell `.000` file; either - // way the lazy super-tile bake below streams to disk through the SAME path. For - // a single file the "dir" is its parent (the worker reads the cell + its `.001…` - // updates from there) and pass 1 is just that one cell. - const single_file = !isDir(io, root_path); - const dir_path = if (single_file) (std.fs.path.dirname(root_path) orelse ".") else root_path; - var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }); - defer dir.close(io); - - // Pass 1: learn each cell's band + bbox. An exchange-set catalogue (CATALOG.031) - // gives every cell's coverage bbox in ONE file (used verbatim), but NOT its - // compilation scale — so the band still comes from a per-cell CSCL peek. The Go - // reference always bands by BandForScale(CompilationScale) (bake.go:592,710), - // never the NOAA name digit: a US2/general-named cell at 1:240000-1:500000 bands - // COASTAL by scale, a US5/harbor-named cell at 1:40000 bands APPROACH — the name - // digit is only a coverage-purpose hint and straddles the bandOf thresholds. - // Fallback (no catalogue): walk the dir and peek each cell (bbox + CSCL). All - // three input paths band identically by bandOf(cscl). - const Bands = engine.bake_enc; - var band_cells: [Bands.bands_fine_to_coarse.len]std.ArrayList(CellEntry) = undefined; - for (&band_cells) |*bc| bc.* = std.ArrayList(CellEntry).empty; - var cell_names = std.ArrayList([]const u8).empty; - var ubox: [4]f64 = .{ 1e9, 1e9, -1e9, -1e9 }; // [w,s,e,n] - var total_cells: usize = 0; - - const via_catalog = if (single_file) sf: { - // Single cell: one entry, band + bbox from a cheap peek (no dir scan). - const base = std.fs.path.basename(root_path); - const bytes = dir.readFileAlloc(io, base, gpa, .unlimited) catch return error.NoGeometry; - defer gpa.free(bytes); - const m = engine.s57.peekMeta(gpa, bytes) orelse return error.NoGeometry; - const bb = m.bounds orelse return error.NoGeometry; - ubox = bb; - total_cells = 1; - try cell_names.append(a, try a.dupe(u8, std.fs.path.stem(base))); - try band_cells[@intFromEnum(Bands.bandOf(m.cscl))].append(a, .{ .path = try a.dupe(u8, base), .bbox = bb }); - break :sf false; - } else cat: { - const cbytes = dir.readFileAlloc(io, "CATALOG.031", gpa, .unlimited) catch break :cat false; - defer gpa.free(cbytes); - const entries = engine.s57.parseCatalog(a, cbytes) orelse break :cat false; - var n_cells: usize = 0; - for (entries) |e| { - if (!e.is_cell) continue; - n_cells += 1; - try cell_names.append(a, e.stem); - total_cells += 1; - const bb = e.bbox orelse continue; - ubox[0] = @min(ubox[0], bb[0]); - ubox[1] = @min(ubox[1], bb[1]); - ubox[2] = @max(ubox[2], bb[2]); - ubox[3] = @max(ubox[3], bb[3]); - // Band by the parsed CSCL (Go BandForScale), NOT the name digit: peek the - // cell for its compilation scale. The catalogue supplies the bbox but no - // scale, so this one cheap read per cell is what keeps the band correct - // (an unreadable cell -> bandOf(0) = approach, the same default the - // dir-walk + single-file paths use). Identical banding to those paths. - const band = bfs: { - const cb = dir.readFileAlloc(io, e.path, gpa, .unlimited) catch break :bfs Bands.bandOf(0); - defer gpa.free(cb); - const m = engine.s57.peekMeta(gpa, cb); - break :bfs Bands.bandOf(if (m) |mm| mm.cscl else 0); - }; - try band_cells[@intFromEnum(band)].append(a, .{ .path = e.path, .bbox = bb }); - } - break :cat n_cells > 0; - }; - - if (!single_file and !via_catalog) { - // De-dup by cell stem: the recursive walk can find the SAME cell under two - // subfolders (a provider ENC_ROOT holds one district subfolder per download, and - // adjacent NOAA districts share boundary cells). Baking a stem twice double-draws - // its features into the overlapping tiles, so the first occurrence wins (a shared - // boundary cell is the same edition across districts). - var seen = std.StringHashMap(void).init(a); - defer seen.deinit(); - var walker = try dir.walk(a); - defer walker.deinit(); - while (try walker.next(io)) |entry| { - if (entry.kind != .file) continue; - if (!std.mem.endsWith(u8, entry.path, ".000")) continue; - if (seen.contains(std.fs.path.stem(std.fs.path.basename(entry.path)))) continue; - const path = try a.dupe(u8, entry.path); - const stem = try a.dupe(u8, std.fs.path.stem(std.fs.path.basename(path))); - try seen.put(stem, {}); - const bytes = dir.readFileAlloc(io, path, gpa, .unlimited) catch continue; - const meta = engine.s57.peekMeta(gpa, bytes); - gpa.free(bytes); - try cell_names.append(a, stem); - total_cells += 1; - // A cell with no peek-bbox has no geometry to bake — skip it (it would - // produce no tiles). The peek bbox is a superset of the parsed bounds, - // so a cell is assigned to every super-tile its real geometry touches. - const m = meta orelse continue; - const bb = m.bounds orelse continue; - ubox[0] = @min(ubox[0], bb[0]); - ubox[1] = @min(ubox[1], bb[1]); - ubox[2] = @max(ubox[2], bb[2]); - ubox[3] = @max(ubox[3], bb[3]); - try band_cells[@intFromEnum(Bands.bandOf(m.cscl))].append(a, .{ .path = path, .bbox = bb }); - } - } - if (total_cells == 0) return error.NoGeometry; - std.debug.print("baking {d} cells from {s} ({s}, rules: {s})\n", .{ total_cells, root_path, if (via_catalog) "via CATALOG.031" else "scanned", if (rules_dir.len == 0) "embedded" else rules_dir }); +// deg → E7 integer. +fn deg7(d: f64) i32 { + return @intFromFloat(@round(d * 1e7)); +} - engine.catalogue.warmUp(); // warm the shared catalogue before parallel portrayal - registerLinestyles(gpa, embeddedLinestyles(a) catch &.{}); // complex-line tessellation table - engine.portray.setQuiet(true); // many threads -> suppress the per-cell stderr +// Distinct band floors, DESCENDING (finest floor first) — the tier ladder. +fn distinctFloorsDesc(a: std.mem.Allocator, cells: []const engine.geometry.plane.Cell) ![]u8 { + var seen = std.AutoHashMap(u8, void).init(a); + for (cells) |c| try seen.put(c.band_floor, {}); + const out = try a.alloc(u8, seen.count()); + var it = seen.keyIterator(); + var i: usize = 0; + while (it.next()) |k| : (i += 1) out[i] = k.*; + std.mem.sort(u8, out, {}, comptime std.sort.desc(u8)); + return out; +} - // Stream the gzipped tiles straight to a temp data file (concatenated into - // out_path at the end) so the whole compressed archive never lives in RAM — - // only the directory + dedup map do. Peak memory is then one band's cells. - const data_tmp = try std.fmt.allocPrint(a, "{s}.data.tmp", .{out_path}); - var data_file = try std.Io.Dir.cwd().createFile(io, data_tmp, .{ .read = true }); - errdefer { - data_file.close(io); - std.Io.Dir.cwd().deleteFile(io, data_tmp) catch {}; - } - var sw = engine.pmtiles.StreamWriter.initFile(gpa, io, data_file); - defer sw.deinit(); - var sounds = std.StringHashMap(void).init(a); - var scamin = std.AutoHashMap(u32, void).init(gpa); - defer scamin.deinit(); - // Sounding-stack + SCAMIN-manifest collection decodes each tile with the codec - // matching the bake format, so MLT bakes get the same sprite-mln sounding - // composites + SCAMIN manifest an MVT bake does. - var sink = BakeSink{ .sw = &sw, .sounds = &sounds, .scamin = &scamin, .a = a, .gpa = gpa, .collect_sounds = collect_sounds, .format = format }; - var baker = Bands.Baker.init(gpa, minzoom, maxzoom, .{ .ctx = &sink, .func = BakeSink.run }); - baker.fillup_dz = fillup_dz; - baker.format = format; - baker.pick_attrs = pick_attrs; - defer baker.deinit(); +// The band floor governing zoom `z`: the largest floor ≤ z, or the coarsest if z is +// below every floor (so zooming out never falls off the bottom). +fn governingTier(floors_desc: []const u8, z: u8) u8 { + for (floors_desc) |t| if (t <= z) return t; + return floors_desc[floors_desc.len - 1]; +} - // Light-reach pre-pass: every cell parsed once (parallel, transient) for its - // attr-based sector-figure reach — the super-tile index + planned-tile spans - // widen each cell's span by it. - const prepass_start = nowSec(); - var all_entries = std.ArrayList(CellEntry).empty; - defer all_entries.deinit(gpa); - for (band_cells) |bc| all_entries.appendSlice(gpa, bc.items) catch break; - const entry_lights: []engine.scene.LightReach = a.alloc(engine.scene.LightReach, all_entries.items.len) catch &.{}; - @memset(entry_lights, .{}); - scanEntryLights(io, dir, all_entries.items, entry_lights); - // Patch each cell's reach back into its band entry (band_cells order == the - // all_entries concatenation order). - { - var k: usize = 0; - for (&band_cells) |*bc| for (bc.items) |*e| { - if (k < entry_lights.len) e.light = entry_lights[k]; - k += 1; +/// Project + clip one tier's faces into the tiles of zoom `z` and stream them to `sw` +/// (layer "partition"). `sa` is a per-zoom arena the caller resets, so only one zoom's +/// tiles are resident. Reuses tile.project/clipPolygon + scene.orientAreaRings (the MVT +/// winding authority) + mvt.encode. +fn emitFacesZoom(sw: *engine.pmtiles.StreamWriter, sa: std.mem.Allocator, faces: []const engine.geometry.plane.OwnedCell, loaded: []const compose.LoadedCov, z: u8, tier: u8) !void { + const mvt = engine.mvt; + const tile = engine.tile; + const scale: f64 = @floatFromInt(@as(u64, 1) << @intCast(z)); + const TileKey = struct { x: u32, y: u32 }; + const TileFeat = struct { polys: std.ArrayList(mvt.Feature), labels: std.ArrayList(mvt.Feature) }; + var tilemap = std.AutoHashMap(TileKey, TileFeat).init(sa); + + for (faces) |face| { + if (face.owned.len == 0) continue; + const lc = loaded[face.index]; + + var fb = [4]f64{ 1e9, 1e9, -1e9, -1e9 }; + for (face.owned) |ring| for (ring) |p| { + const lon = @as(f64, @floatFromInt(p.x)) / 1e7; + const lat = @as(f64, @floatFromInt(p.y)) / 1e7; + fb[0] = @min(fb[0], lon); + fb[1] = @min(fb[1], lat); + fb[2] = @max(fb[2], lon); + fb[3] = @max(fb[3], lat); }; - } - // Cross-pack best-available: a peer pack's coverage (bake --existing) suppresses - // this pack's coarser cells where the peer is finer — stops an overview pack - // painting over the peer's sectors. Empty (no --existing) = a plain bake. - baker.context = loadContextCoverage(io, a, existing_path); - if (baker.context.len > 0) - std.debug.print(" cross-pack context: {d} peer coverage cell(s) from {s}\n", .{ baker.context.len, existing_path }); - std.debug.print(" light-reach pre-pass: {d} cell(s) scanned ({d}s)\n", .{ all_entries.items.len, nowSec() - prepass_start }); - - // Pass 2: bake each band finest → coarsest (best-band dedup via baker.emitted). - // Within a band, walk spatial "super-tiles" (one tile at zoom zs = zlo - SUPER_DZ): - // load ONLY the cells overlapping each super-tile (parse + portray in parallel), - // generate that super-tile's tiles (clipped, parallel), and keep parsed cells in - // a small LRU so neighbouring super-tiles reuse them (cells span a few super-tiles - // and are parsed once). Peak memory is the busiest super-tile's cells + the LRU - // budget — not the whole band. Coarse bands have few (huge) cells and a tiny grid, - // so they fall back to loading them together. - std.debug.print(" (lazy bake: lru={d} cells, super-dz={d})\n", .{ lru_budget, super_dz }); - g_bake_start = nowSec(); - // The coarsest populated band gets .extend_min (fill down to minzoom — the - // live tileRefs coarsest-band fallback); every other populated band defers its - // floor into the next-coarser pass (.defer_down, the band handoff). - var coarsest_pop: ?Bands.Band = null; - for (Bands.bands_fine_to_coarse) |band| { - if (band_cells[@intFromEnum(band)].items.len > 0) coarsest_pop = band; - } - // Band label (host §3): count the passes that will actually bake — a band with - // no cells still runs when the next-finer band deferred its floor tiles into it. - var band_count: u8 = 0; - { - var carry_n: usize = 0; - for (Bands.bands_fine_to_coarse) |band| { - const own_n = band_cells[@intFromEnum(band)].items.len; - const floor: Bands.FloorMode = if (coarsest_pop == band) .extend_min else .defer_down; - if (Bands.passHasWork(band, minzoom, maxzoom, own_n, carry_n, floor)) band_count += 1; - carry_n = if (own_n > 0 and floor == .defer_down and Bands.floorDeferred(band, minzoom, maxzoom)) own_n else 0; - } - } - baker.band_count = band_count; - var carry: ?CarryState = null; // the finer band's cells riding into this pass - var band_ord: u8 = 0; - var done_cells: usize = 0; - for (Bands.bands_fine_to_coarse) |band| { - const entries = band_cells[@intFromEnum(band)].items; - const carry_entries: []const CellEntry = if (carry) |c| c.entries else &.{}; - const floor: Bands.FloorMode = if (coarsest_pop == band) .extend_min else .defer_down; - // Whether this band's own floor rides on into the NEXT pass (its cells must - // then outlive this one). .extend_min never defers — nothing coarser runs. - const deferred = entries.len > 0 and floor == .defer_down and Bands.floorDeferred(band, minzoom, maxzoom); - if (!Bands.passHasWork(band, minzoom, maxzoom, entries.len, carry_entries.len, floor)) { - // Nothing bakes here. Drop a consumed-less carry; when this band still - // defers (its whole clamped range IS the deferred floor), hand its - // entries on with fresh state — the next pass loads them itself. - if (carry) |*c| c.deinit(gpa); - carry = if (deferred) try CarryState.initEmpty(gpa, entries) else null; - done_cells += entries.len; - continue; - } - const parses0 = g_parses.load(.monotonic); - const band_start = nowSec(); - baker.band_index = band_ord; - band_ord += 1; - - // Effective zoom span (deferral/extension applied) — drives the super-tile - // zoom zs. A carry-only pass bakes just the deferred floor (this band's max). - const zr = Bands.bandZooms(band); - var zlo = @max(minzoom, zr.min); - const zhi = @min(maxzoom, zr.max); - switch (floor) { - .defer_down => if (zlo == zr.min and zlo <= zhi) { - zlo += 1; - }, - .extend_min => zlo = minzoom, - } - const z_first: u8 = if (entries.len > 0 and zlo <= zhi) zlo else zr.max; - const zs: u8 = if (z_first >= super_dz) z_first - super_dz else 0; - - // Combined pass cells: own first (indices < n_own), then the finer band's - // carry, then the COARSE RIDERS — every strictly-coarser band's cells, - // joining this pass's tiles as extra contributors (Baker.rider_start) so a - // finer-band tile at a cell-bbox edge carries the coarser neighbour's - // content (the composite clips them to the ground finer coverage leaves). - // Riders never enumerate tiles; they load/evict through the same - // super-tile LRU as everything else. Every state array below is - // index-aligned with this slice. - var rider_entries = std.ArrayList(CellEntry).empty; - defer rider_entries.deinit(gpa); - { - // NEXT-COARSER POPULATED band only. All-coarser riders made the giant - // general/overview cells overlap EVERY super-tile of a fine pass — - // thousands of LRU-thrashing re-parses ("painfully slow" district - // bakes) — while the edge-hole they'd fill (a tile where NO nearer - // band covers either) is vanishingly rare: harbor cells sit inside - // approach coverage, approach inside coastal, and each pass's own - // holes are filled by ITS immediate coarser neighbour in ITS pass. - var past_self = false; - for (Bands.bands_fine_to_coarse) |b2| { - if (b2 == band) { - past_self = true; - continue; - } - if (!past_self) continue; - const items = band_cells[@intFromEnum(b2)].items; - if (items.len == 0) continue; - rider_entries.appendSlice(gpa, items) catch {}; - break; - } - } - const n_own = entries.len; - const n_nonrider = n_own + carry_entries.len; - const n_all = n_nonrider + rider_entries.items.len; - const pass_entries = try a.alloc(CellEntry, n_all); - @memcpy(pass_entries[0..n_own], entries); - @memcpy(pass_entries[n_own..n_nonrider], carry_entries); - @memcpy(pass_entries[n_nonrider..], rider_entries.items); - - // Inverted index: super-tile (sx,sy at zs) → the cell indices overlapping it. - var stmap = std.AutoHashMap(u64, std.ArrayList(u32)).init(gpa); - defer { - var vit = stmap.valueIterator(); - while (vit.next()) |v| v.deinit(gpa); - stmap.deinit(); - } - for (pass_entries[0..n_nonrider], 0..) |e, i| { - const nw = lonLatToTile(e.bbox[0], e.bbox[3], zs); - const se = lonLatToTile(e.bbox[2], e.bbox[1], zs); - var sy = nw[1]; - while (sy <= se[1]) : (sy += 1) { - var sx = nw[0]; - while (sx <= se[0]) : (sx += 1) { - const k = (@as(u64, sy) << 32) | sx; // row-major (spatial locality) - const gop = try stmap.getOrPut(k); - if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(u32).empty; - try gop.value_ptr.append(gpa, @intCast(i)); - } - } - // Sector-figure reach: ALSO assign the cell to the super-tiles its - // light legs/arcs reach into (buildTileMap addresses those tiles via - // its reach ring, but only for cells loaded in that super-tile's - // pass). Margin in zs-tiles: the ground legs scale with 2^z so their - // zs-tile count is zoom-independent; the display-mm figures span at - // most one tile at the pass's coarsest zoom < one zs-tile; +1 covers - // the z-tile -> super-tile rounding. Raw-span members are skipped. - const lb = e.light.bbox orelse continue; - const m: u32 = 1 + @as(u32, @intFromFloat(@ceil(engine.scene.lightReachTiles(e.light.range_m, zs, (lb[1] + lb[3]) * 0.5)))); - const max_idx: u32 = @intCast((@as(u64, 1) << @intCast(zs)) - 1); - const lnw = lonLatToTile(lb[0], lb[3], zs); - const lse = lonLatToTile(lb[2], lb[1], zs); - const rxlo = lnw[0] -| m; - const rxhi = @min(lse[0] +| m, max_idx); - var ry = lnw[1] -| m; - const ryhi = @min(lse[1] +| m, max_idx); - while (ry <= ryhi) : (ry += 1) { - var rx = rxlo; - while (rx <= rxhi) : (rx += 1) { - if (rx >= nw[0] and rx <= se[0] and ry >= nw[1] and ry <= se[1]) continue; // raw span has it - const k = (@as(u64, ry) << 32) | rx; - const gop = try stmap.getOrPut(k); - if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(u32).empty; - try gop.value_ptr.append(gpa, @intCast(i)); - } - } - } - // Riders join only super-tiles that ALREADY exist (they never create one). - for (pass_entries[n_nonrider..], n_nonrider..) |e, i| { - const nw = lonLatToTile(e.bbox[0], e.bbox[3], zs); - const se = lonLatToTile(e.bbox[2], e.bbox[1], zs); - var sy = nw[1]; - while (sy <= se[1]) : (sy += 1) { - var sx = nw[0]; - while (sx <= se[0]) : (sx += 1) { - const k = (@as(u64, sy) << 32) | sx; - if (stmap.getPtr(k)) |lst| try lst.append(gpa, @intCast(i)); - } - } - } - - const stkeys = try gpa.alloc(u64, stmap.count()); - defer gpa.free(stkeys); - { - var kit = stmap.keyIterator(); - var j: usize = 0; - while (kit.next()) |kp| : (j += 1) stkeys[j] = kp.*; - } - std.mem.sort(u64, stkeys, {}, struct { - fn lt(_: void, x: u64, y: u64) bool { - return x < y; - } - }.lt); - g_prog.band = @tagName(band); - g_prog.st = 0; - g_prog.st_total = stkeys.len; - g_prog.band_start = nowSec(); - - // Per-band tile total for the progress bar (host §3): sum the planned tiles - // over the band's super-tiles (a planned estimate from the cheap peek bboxes, - // dedup'd against finer bands via the baker's `emitted` set, which is complete - // here since bands bake finest→coarsest). Set it on the baker so bakeBand - // reports done/total as a per-band percentage instead of "total 0 = unknown". - // A super-tile's index list is ascending, so its own cells precede its carry - // cells — the own_len split plannedTiles/bakeBand key on. - baker.band_base = baker.count; - var band_total: usize = 0; - for (stkeys) |stk| { - const cells = stmap.get(stk).?.items; - var spans = std.ArrayList(Bands.CellSpan).empty; - defer spans.deinit(gpa); - var st_own: usize = 0; - for (cells) |ci| { - if (ci >= n_nonrider) continue; // riders don't enumerate tiles - const pe = pass_entries[ci]; - spans.append(gpa, .{ .bounds = pe.bbox, .light_bbox = pe.light.bbox, .light_range_m = pe.light.range_m }) catch continue; - if (ci < n_own) st_own += 1; - } - band_total += baker.plannedTiles(band, spans.items, st_own, floor, .{ .zs = zs, .sx = @intCast(stk & 0xFFFFFFFF), .sy = @intCast(stk >> 32) }); - } - baker.band_total = band_total; - std.debug.print("\n band {s}: {d}+{d} cells (+{d} coarse riders), {d} super-tiles (z{d}-{d}), ~{d} tiles — baking…\n", .{ @tagName(band), entries.len, carry_entries.len, rider_entries.items.len, stkeys.len, @min(z_first, zhi), zhi, band_total }); - - // Per-pass caches (index-aligned with `pass_entries`): portrayal is RESIDENT - // (computed once — the expensive Lua step — and cheap to keep); geometry is - // the heavy part, held in an LRU and re-parsed (NOT re-portrayed) on demand. - // The carry block adopts the finer band's surviving state wholesale. - const portray = try gpa.alloc(?CellPortray, n_all); - defer gpa.free(portray); - @memset(portray, null); - const geom = try gpa.alloc(?CellGeom, n_all); - defer gpa.free(geom); - @memset(geom, null); - const used = try gpa.alloc(u64, n_all); // LRU tick per cell (geometry) - defer gpa.free(used); - @memset(used, 0); - const gave_up = try gpa.alloc(bool, n_all); // parse failed: don't retry - defer gpa.free(gave_up); - @memset(gave_up, false); - var n_geom: usize = 0; // loaded-geometry count (the LRU target) - if (carry) |c| { - @memcpy(portray[n_own..n_nonrider], c.portray); - @memcpy(geom[n_own..n_nonrider], c.geom); - @memcpy(gave_up[n_own..n_nonrider], c.gave_up); - gpa.free(c.portray); - gpa.free(c.geom); - gpa.free(c.gave_up); - carry = null; // consumed — the combined arrays own the state now - for (geom[n_own..]) |g| { - if (g != null) n_geom += 1; - } - } - var tick: u64 = 0; - - for (stkeys, 0..) |stk, st_i| { - g_prog.st = st_i + 1; - const cells = stmap.get(stk).?.items; - const sx: u32 = @intCast(stk & 0xFFFFFFFF); - const sy: u32 = @intCast(stk >> 32); - - // (Re)load this super-tile's cells whose geometry isn't resident — each - // worker reads its own cell's bytes + parses (+ portrays once) in - // parallel, so cold reads aren't serialised. A cell already portrayed is - // only re-parsed, never re-portrayed. - var miss = std.ArrayList(u32).empty; - defer miss.deinit(gpa); - for (cells) |ci| if (geom[ci] == null and !gave_up[ci]) { - // Portrayal computed by ANY earlier pass (rider/carry/own) is - // reused — the worker then only re-parses geometry. - if (portray[ci] == null) { - if (pcache.get(pass_entries[ci].path)) |pc| portray[ci] = pc; - } - miss.append(gpa, ci) catch {}; - }; - if (miss.items.len > 0) { - var lw = LoadWork{ .cells = miss.items, .entries = pass_entries, .dir = dir, .io = io, .portray = portray, .geom = geom, .rules_dir = rules_dir, .gpa = gpa }; - Bands.parallelFor(gpa, miss.items.len, &lw, LoadWork.run); - for (miss.items) |ci| { - if (geom[ci] != null) n_geom += 1 else gave_up[ci] = true; - if (portray[ci]) |pc| { - const gop = pcache.getOrPut(pass_entries[ci].path) catch continue; - if (!gop.found_existing) gop.value_ptr.* = pc; - } - } - } - tick += 1; - for (cells) |ci| used[ci] = tick; - - // Generate this super-tile's tiles from its loaded cells (clipped, parallel): - // pair each cell's geometry with its resident portrayal. `cells` ascends, so - // own cells land before carry cells; st_own is bakeBand's own_len split. - var subset = std.ArrayList(Bands.Backend).empty; - defer subset.deinit(gpa); - var st_own: usize = 0; - var st_nonrider: usize = 0; - for (cells) |ci| if (geom[ci]) |g| { - const p = portray[ci]; - subset.append(gpa, .{ - .cell = g.cell, - .portrayal = if (p) |pp| pp.base else null, - .portrayal_plain = if (p) |pp| pp.plain else null, - .portrayal_simplified = if (p) |pp| pp.simplified else null, - .geo = g.geo, - .geo_world = g.geo_world, - .feat_bbox = g.feat_bbox, - .bounds = if (p) |pp| pp.bounds else pass_entries[ci].bbox, - .cscl = g.cell.params.cscl, - .coverage = g.coverage, - .scamins = g.scamins, - .light_bbox = if (p) |pp| pp.light.bbox else pass_entries[ci].light.bbox, - .light_range_m = if (p) |pp| pp.light.range_m else pass_entries[ci].light.range_m, - }) catch continue; - if (ci < n_own) st_own += 1; - if (ci < n_nonrider) st_nonrider += 1; - }; - baker.rider_start = st_nonrider; - baker.bakeBand(band, subset.items, st_own, floor, .{ .zs = zs, .sx = sx, .sy = sy }, prog, progress_user) catch {}; - - // Evict least-recently-used GEOMETRY beyond the budget (never this - // super-tile's, which carry the current tick). Portrayal stays resident. - while (n_geom > lru_budget) { - var victim: ?usize = null; - var best: u64 = std.math.maxInt(u64); - for (geom, 0..) |g, ci| { - if (g != null and used[ci] < best) { - best = used[ci]; - victim = ci; - } - } - const v = victim orelse break; - if (used[v] == tick) break; // only the current super-tile's cells remain - if (geom[v]) |*g| { - g.cell.deinit(); - if (g.geo_arena) |p| { - p.deinit(); - gpa.destroy(p); - } - } - geom[v] = null; - n_geom -= 1; - } - } + const w_tl = tile.lonLatToWorld(fb[0], fb[3]); + const w_br = tile.lonLatToWorld(fb[2], fb[1]); + const tx0 = compose.worldAxisToTile(w_tl[0], scale); + const tx1 = compose.worldAxisToTile(w_br[0], scale); + const ty0 = compose.worldAxisToTile(w_tl[1], scale); + const ty1 = compose.worldAxisToTile(w_br[1], scale); + + const props = try sa.dupe(mvt.Prop, &.{ + .{ .key = "cell", .value = .{ .string = lc.name } }, + .{ .key = "cscl", .value = .{ .int = lc.cscl } }, + .{ .key = "band", .value = .{ .int = @intFromEnum(engine.bake_enc.bandOf(lc.cscl)) } }, + .{ .key = "tier", .value = .{ .int = tier } }, + .{ .key = "oi", .value = .{ .int = @intCast(face.index) } }, + .{ .key = "color", .value = .{ .string = DEBUG_PALETTE[face.index % DEBUG_PALETTE.len] } }, + }); - // Fill-down: an area charted only by bands FINER than the globally-coarsest - // populated band (e.g. a bay with just coastal/approach cells while the - // pack's overview cells lie elsewhere) gets no tiles below this band's - // window — the district-pack "empty z6–z8 hole". extend_min only reaches - // the ONE coarsest band; here each defer_down band fills its below-window - // zooms where no strictly-coarser band covers (bake_enc.bakeFillDown, - // mirroring the live tileRefs fallbackBand). Only cells not blanketed by a - // coarser band participate, so the extra load stays bounded. - if (floor == .defer_down and n_own > 0 and Bands.fillDownZooms(band, minzoom, maxzoom) != null) { - var coarser = std.ArrayList(Bands.CoarserBox).empty; - defer coarser.deinit(gpa); - for (Bands.bands_fine_to_coarse) |cb| { - if (@intFromEnum(cb) <= @intFromEnum(band)) continue; // strictly coarser - const mz = Bands.bandZooms(cb).max; - for (band_cells[@intFromEnum(cb)].items) |ce| coarser.append(gpa, .{ .bbox = ce.bbox, .max_z = mz }) catch {}; - } - var fd = std.ArrayList(u32).empty; // own cells with area a coarser band leaves uncovered - defer fd.deinit(gpa); - for (0..n_own) |ci| { - if (!Bands.coveredByCoarser(pass_entries[ci].bbox, coarser.items)) fd.append(gpa, @intCast(ci)) catch {}; - } - if (fd.items.len > 0) { - // (Re)load the uncovered cells' geometry (portrayal is resident) — - // fill-down tiles are broad low-zoom, so no super-tile clip applies. - var miss = std.ArrayList(u32).empty; - defer miss.deinit(gpa); - for (fd.items) |ci| if (geom[ci] == null and !gave_up[ci]) { - if (portray[ci] == null) { - if (pcache.get(pass_entries[ci].path)) |pc| portray[ci] = pc; - } - miss.append(gpa, ci) catch {}; - }; - if (miss.items.len > 0) { - var lw = LoadWork{ .cells = miss.items, .entries = pass_entries, .dir = dir, .io = io, .portray = portray, .geom = geom, .rules_dir = rules_dir, .gpa = gpa }; - Bands.parallelFor(gpa, miss.items.len, &lw, LoadWork.run); - for (miss.items) |ci| { - if (geom[ci] != null) n_geom += 1 else gave_up[ci] = true; - if (portray[ci]) |pc| { - const gop = pcache.getOrPut(pass_entries[ci].path) catch continue; - if (!gop.found_existing) gop.value_ptr.* = pc; - } - } + var tx = tx0; + while (tx <= tx1) : (tx += 1) { + var ty = ty0; + while (ty <= ty1) : (ty += 1) { + var parts = std.ArrayList([]const mvt.Point).empty; + for (face.owned) |ring| { + const proj = try sa.alloc(mvt.Point, ring.len); + for (ring, 0..) |p, k| proj[k] = tile.project(@as(f64, @floatFromInt(p.x)) / 1e7, @as(f64, @floatFromInt(p.y)) / 1e7, z, tx, ty, tile.EXTENT); + const clipped = try tile.clipPolygon(sa, proj, tile.Box.default(tile.EXTENT, 256)); + if (clipped.len >= 3) try parts.append(sa, clipped); } - var subset = std.ArrayList(Bands.Backend).empty; - defer subset.deinit(gpa); - for (fd.items) |ci| if (geom[ci]) |g| { - const p = portray[ci]; - subset.append(gpa, .{ - .cell = g.cell, - .portrayal = if (p) |pp| pp.base else null, - .portrayal_plain = if (p) |pp| pp.plain else null, - .portrayal_simplified = if (p) |pp| pp.simplified else null, - .geo = g.geo, - .geo_world = g.geo_world, - .feat_bbox = g.feat_bbox, - .bounds = if (p) |pp| pp.bounds else pass_entries[ci].bbox, - .cscl = g.cell.params.cscl, - .coverage = g.coverage, - .scamins = g.scamins, - .light_bbox = if (p) |pp| pp.light.bbox else pass_entries[ci].light.bbox, - .light_range_m = if (p) |pp| pp.light.range_m else pass_entries[ci].light.range_m, - }) catch continue; + if (parts.items.len == 0) continue; + const oriented = try engine.mvt.orientAreaRings(sa, parts.items); + const gop = try tilemap.getOrPut(.{ .x = tx, .y = ty }); + if (!gop.found_existing) gop.value_ptr.* = .{ .polys = .empty, .labels = .empty }; + try gop.value_ptr.polys.append(sa, .{ .geom_type = .polygon, .parts = oriented, .properties = props }); + + // Label this face's part IN THIS tile, at the centre of its clipped + // extent, so the owning cell name is readable wherever the face shows + // (layer "labels"). MapLibre collision-dedups the repeats across tiles. + var lb = [4]i32{ std.math.maxInt(i32), std.math.maxInt(i32), std.math.minInt(i32), std.math.minInt(i32) }; + for (oriented) |ring| for (ring) |p| { + lb[0] = @min(lb[0], p.x); + lb[1] = @min(lb[1], p.y); + lb[2] = @max(lb[2], p.x); + lb[3] = @max(lb[3], p.y); }; - baker.bakeFillDown(band, subset.items, coarser.items, prog, progress_user) catch {}; + const lpt = try sa.alloc(mvt.Point, 1); + lpt[0] = .{ .x = @divTrunc(lb[0] + lb[2], 2), .y = @divTrunc(lb[1] + lb[3], 2) }; + const lparts = try sa.alloc([]const mvt.Point, 1); + lparts[0] = lpt; + try gop.value_ptr.labels.append(sa, .{ .geom_type = .point, .parts = lparts, .properties = try sa.dupe(mvt.Prop, &.{.{ .key = "cell", .value = .{ .string = lc.name } }}) }); } } - - // The carry block's ride ends here: free the finer band's remaining - // geometry + portrayal. The own block either becomes the NEXT pass's carry - // (deferred — its floor tiles bake there) or is freed with it. - for (geom[n_own..], n_own..) |g, ci| if (g != null) { - if (geom[ci]) |*gg| { - gg.cell.deinit(); - if (gg.geo_arena) |p| { - p.deinit(); - gpa.destroy(p); - } - } - }; - // Portrayal arenas are owned by the bake-lifetime pcache now — nothing - // to free per pass (geometry above still is). - if (deferred) { - const po = try gpa.alloc(?CellPortray, n_own); - @memcpy(po, portray[0..n_own]); - const ge = try gpa.alloc(?CellGeom, n_own); - @memcpy(ge, geom[0..n_own]); - const gu = try gpa.alloc(bool, n_own); - @memcpy(gu, gave_up[0..n_own]); - carry = .{ .entries = entries, .portray = po, .geom = ge, .gave_up = gu }; - } else { - for (geom[0..n_own], 0..) |g, ci| if (g != null) { - if (geom[ci]) |*gg| { - gg.cell.deinit(); - if (gg.geo_arena) |p| { - p.deinit(); - gpa.destroy(p); - } - } - }; - // Portrayal arenas are owned by the bake-lifetime pcache (freed once - // when the bake returns) — do NOT free them here. These own cells' - // portrayal is registered in pcache at load, so freeing it per pass - // double-frees it at pcache teardown (the coarsest band is always - // non-deferred, so this else branch runs every bake). - } - const parses = g_parses.load(.monotonic) - parses0; - const band_secs = nowSec() - band_start; - std.debug.print( - "\r band {s} done: {d}+{d} cells, {d} super-tiles, {d} tiles total, {d} parses ({d:.2}x), {d}s\n", - .{ @tagName(band), entries.len, carry_entries.len, stkeys.len, baker.count, parses, @as(f64, @floatFromInt(parses)) / @as(f64, @floatFromInt(@max(n_all, 1))), band_secs }, - ); - done_cells += entries.len; - if (prog) |cb| cb(progress_user, 0, done_cells, total_cells, band_ord - 1, band_count, @tagName(band).ptr); } - if (carry) |*c| c.deinit(gpa); // overview never defers, but stay leak-proof - // Assemble out_path = prefix (header + directory + metadata) ++ the data - // section streamed to data_tmp. The prefix is small; the data is copied in - // bounded chunks (positional reads, no whole-archive buffer), then the temp - // file is removed. - // PMTiles metadata: vector_layers (TileJSON) + the distinct SCAMIN denominators - // (when collected) so the client builds its per-SCAMIN bucket layers at load. - var scamin_vals = std.ArrayList(u32).empty; // returned in RootBake (lives in `a`) - { - var it = scamin.keyIterator(); - while (it.next()) |k| try scamin_vals.append(a, k.*); - std.mem.sort(u32, scamin_vals.items, {}, std.sort.asc(u32)); + var it = tilemap.iterator(); + while (it.next()) |kv| { + var layers = std.ArrayList(mvt.Layer).empty; + if (kv.value_ptr.polys.items.len > 0) try layers.append(sa, .{ .name = "partition", .features = kv.value_ptr.polys.items }); + if (kv.value_ptr.labels.items.len > 0) try layers.append(sa, .{ .name = "labels", .features = kv.value_ptr.labels.items }); + if (layers.items.len == 0) continue; + const enc = try mvt.encode(sa, .{ .layers = layers.items }); + try sw.add(z, kv.key_ptr.x, kv.key_ptr.y, enc); } - const meta = try engine.scene.metadataJson(a, scamin_vals.items); - const opts = engine.pmtiles.WriteOptions{ - .metadata_json = meta, - .min_lon_e7 = toE7(ubox[0]), - .min_lat_e7 = toE7(ubox[1]), - .max_lon_e7 = toE7(ubox[2]), - .max_lat_e7 = toE7(ubox[3]), - .tile_type = if (format == .mlt) .mlt else .mvt, - }; - const pre = try sw.prefix(a, opts); - var file = try std.Io.Dir.cwd().createFile(io, out_path, .{}); - try file.writeStreamingAll(io, pre); - { - const chunk = try a.alloc(u8, 1 << 20); // 1 MiB - defer a.free(chunk); - var pos: u64 = 0; - while (pos < sw.data_len) { - const want: usize = @intCast(@min(@as(u64, chunk.len), sw.data_len - pos)); - _ = try data_file.readPositionalAll(io, chunk[0..want], pos); - try file.writeStreamingAll(io, chunk[0..want]); - pos += want; - } - } - file.close(io); - data_file.close(io); - std.Io.Dir.cwd().deleteFile(io, data_tmp) catch {}; - - var stacks = std.ArrayList([]const u8).empty; - var it = sounds.keyIterator(); - while (it.next()) |k| try stacks.append(a, k.*); - return .{ .bounds = ubox, .cells = cell_names.items, .sounds = stacks.items, .scamin = scamin_vals.items }; -} - -/// lon/lat (deg) -> web-mercator tile (x, y) at zoom z, clamped to the valid range. -fn lonLatToTile(lon: f64, lat: f64, z: u8) [2]u32 { - const w = engine.tile.lonLatToWorld(lon, lat); // normalised [0,1], y down - const scale: f64 = @floatFromInt(@as(u64, 1) << @intCast(z)); - const max_idx: f64 = scale - 1.0; - const fx = std.math.clamp(@floor(w[0] * scale), 0.0, max_idx); - const fy = std.math.clamp(@floor(w[1] * scale), 0.0, max_idx); - return .{ @intFromFloat(fx), @intFromFloat(fy) }; -} - -/// Degrees -> PMTiles E7 fixed-point. -fn toE7(v: f64) i32 { - return @intFromFloat(@round(v * 1e7)); } diff --git a/src/capi.zig b/src/capi.zig index a05f19a..5dcae29 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -1,15 +1,20 @@ //! C ABI for libtile57.a — a thin shim over the Zig engine API (chart.zig). //! -//! Contract: POD across the seam (ptr/len + status codes); Zig errors, slices -//! and optionals stay inside chart.zig. Public header: ../../include/tile57.h. -//! The opaque `tile57_chart` is a `*chart.Chart`. +//! Contract: POD across the seam. Every export that can fail returns a tile57_status +//! (0 = OK), takes an optional caller-owned tile57_error* it fills on failure, +//! and defines its out-parameters on every return (result on OK, NULL/0 +//! otherwise). Zig errors, slices and optionals stay inside chart.zig. Public +//! header: ../../include/tile57.h. The opaque `tile57` is a `*chart.Chart`; +//! the opaque `tile57_compose` is a `*compose.ComposeSource`. const std = @import("std"); const chart = @import("chart.zig"); const s57 = @import("s57"); -const bundle = @import("bundle"); // the whole chart-bundle pipeline (tiles + assets + manifest) -const chartstyle = @import("assets").chartstyle; -const assets = @import("assets"); +const bundle = @import("bundle"); // portrayal-asset emitters + the partition debug bake +const compose = @import("compose"); // the runtime tile compositor (tile57_compose_*) +const mariner = @import("style").mariner; +const style = @import("style"); +const errors = @import("errors"); // the engine error taxonomy + describe() // The S-52 ColorProfiles/colorProfile.xml baked into the library (build.zig), so // the style C ABI generates colortables + a base style template with no on-disk // catalogue. Symbols/linestyles are NOT embedded here (only the bake exe needs them). @@ -21,130 +26,404 @@ const colorprofile_registry = @import("colorprofile_registry"); const gpa = std.heap.smp_allocator; const Chart = chart.Chart; -// Wall-clock time for "today" date resolution in tile57_build_style. Zig 0.16 +// Wall-clock time for "today" date resolution in tile57_style_build. Zig 0.16 // keeps the clock behind Io; the lib links libc, so call time(3) directly. extern fn time(tloc: ?*c_long) callconv(.c) c_long; // Keep in sync with the TILE57_VERSION_* macros in tile57.h. -const version_string = "0.1.0"; +const version_string = "0.3.0"; fn spanOpt(s: ?[*:0]const u8) ?[]const u8 { return if (s) |p| std.mem.span(p) else null; } -/// Return the library version string ("0.1.0"). +// ---- error model (mirrors tile57_status / tile57_error in tile57.h) --------- + +// Keep in sync with the tile57_status enum in tile57.h. +const Status = enum(c_int) { ok = 0, badarg, io, parse, nomem, unsupported, render, internal }; + +// Mirrors tile57_error in tile57.h: a caller-owned status + fixed message buffer. +const ERROR_MSG_MAX = 256; +const CError = extern struct { status: c_int, message: [ERROR_MSG_MAX]u8 }; + +const OK: c_int = @intFromEnum(Status.ok); + +// Map an engine error (errors.Error) to a tile57_status. std IO errors that can +// surface directly from file ops map to .io; anything unrecognised is .internal. +fn statusOf(e: anyerror) Status { + return switch (e) { + error.OutOfMemory => .nomem, + error.Unsupported => .unsupported, + error.RenderFailed, error.TileGen => .render, + error.InvalidCell, error.InvalidArchive, error.InvalidPartition => .parse, + // Specific S-57 / ISO 8211 parse failures, propagated so the message + // carries the exact reason (e.g. "BadLeader", "UnknownRUIN"). + error.ShortLeader, + error.BadLeader, + error.BadAsciiInt, + error.BadAsciiDigit, + error.MissingFieldTerminator, + error.FieldOutOfBounds, + error.ModifyMissingSpatial, + error.ModifyMissingFeature, + error.UnknownRUIN, + error.BadFeatureRecord, + => .parse, + error.NotFound, error.IoFailed => .io, + error.FileNotFound, error.AccessDenied, error.NotDir, error.IsDir, error.OpenFailed => .io, + else => .internal, + }; +} + +// Fill an optional caller-provided tile57_error (NULL to ignore) with a status + +// message; the message is copied, truncated to fit, and NUL-terminated. +fn setError(err: ?*CError, status: Status, msg: []const u8) void { + const dst = err orelse return; + dst.status = @intFromEnum(status); + const n = @min(msg.len, ERROR_MSG_MAX - 1); + @memcpy(dst.message[0..n], msg[0..n]); + dst.message[n] = 0; +} + +// Report a Zig error: set `err` (if any) with the mapped status + describe() +// message, and return the status code. +fn fail(err: ?*CError, e: anyerror) c_int { + const s = statusOf(e); + setError(err, s, errors.describe(e)); + return @intFromEnum(s); +} + +// Like fail, but prefix the message with a context string (e.g. the file path): +// "US5MD1MC.000: malformed ISO 8211 leader". Truncated to fit. +fn failCtx(err: ?*CError, e: anyerror, context: []const u8) c_int { + const s = statusOf(e); + if (err) |dst| { + dst.status = @intFromEnum(s); + const msg = std.fmt.bufPrint(dst.message[0 .. ERROR_MSG_MAX - 1], "{s}: {s}", .{ context, errors.describe(e) }) catch blk: { + // Context + reason overflowed the buffer; keep the reason alone. + const r = errors.describe(e); + const n = @min(r.len, ERROR_MSG_MAX - 1); + @memcpy(dst.message[0..n], r[0..n]); + break :blk dst.message[0..n]; + }; + dst.message[msg.len] = 0; + } + return @intFromEnum(s); +} + +// Report a specific status with a literal message. +fn failWith(err: ?*CError, status: Status, msg: []const u8) c_int { + setError(err, status, msg); + return @intFromEnum(status); +} + +// Validate + zero a (bytes, len) out-parameter pair. Every buffer-returning +// export runs this first, so outs are defined (NULL/0) on every return path. +fn bytesOut(out: ?*?[*]u8, out_len: ?*usize) error{BadArg}!struct { *?[*]u8, *usize } { + const o = out orelse return error.BadArg; + const n = out_len orelse return error.BadArg; + o.* = null; + n.* = 0; + return .{ o, n }; +} + +// ---- export allocations ------------------------------------------------------ +// Every buffer handed across the ABI is length-prefixed: a 16-byte header (the +// total allocation size in its first usize) sits before the returned pointer, so +// tile57_free needs only the pointer — the classic malloc shape — and the payload +// stays 16-aligned. +const EXPORT_HDR: usize = 16; + +fn exportAlloc(len: usize) ?[*]u8 { + const total = EXPORT_HDR + len; + const raw = gpa.alignedAlloc(u8, .@"16", total) catch return null; + std.mem.writeInt(usize, raw[0..@sizeOf(usize)], total, .little); + return raw.ptr + EXPORT_HDR; +} + +// Hand an engine-owned buffer across the ABI through (out, out_len): copy it into +// an export allocation and free the engine buffer. Returns OK, or NOMEM with the +// outs left NULL/0. +fn exportOut(err: ?*CError, o: *?[*]u8, n: *usize, bytes: []u8) c_int { + defer chart.freeBytes(bytes); + const p = exportAlloc(bytes.len) orelse return failWith(err, .nomem, "out of memory"); + @memcpy(p[0..bytes.len], bytes); + o.* = p; + n.* = bytes.len; + return OK; +} + +const bad_out = "out/out_len must not be null"; + +/// Return a static, human-readable string for a tile57_status. +export fn tile57_status_str(status: c_int) callconv(.c) [*:0]const u8 { + return switch (status) { + @intFromEnum(Status.ok) => "ok", + @intFromEnum(Status.badarg) => "invalid argument", + @intFromEnum(Status.io) => "I/O error", + @intFromEnum(Status.parse) => "malformed input", + @intFromEnum(Status.nomem) => "out of memory", + @intFromEnum(Status.unsupported) => "unsupported input", + @intFromEnum(Status.render) => "render failed", + @intFromEnum(Status.internal) => "internal error", + else => "unknown error", + }; +} + +/// Return the library version string ("0.3.0"). export fn tile57_version() callconv(.c) [*:0]const u8 { return version_string; } -// One ENC cell's bytes for the C ABI. Mirrors tile57_cell in tile57.h. -const CellInput = extern struct { - base: [*]const u8, - base_len: usize, - updates: ?[*]const [*]const u8, - update_lens: ?[*]const usize, - update_count: usize, - // Source cell name (NUL-terminated, e.g. "US4MD81M") for the pick report's - // "source cell" badge. NULL/"" = omitted. Appended after the original fields so - // a host that zero-inits the struct gets the no-name (NULL) behaviour. - name: ?[*:0]const u8 = null, -}; +// =========================================================================== +// 3. Bake — ENC source data in, per-cell PMTiles out (see tile57.h) +// =========================================================================== + +/// The per-cell metadata of the S-57 data at `path` (one .000 or a whole ENC_ROOT) +/// as a JSON array — name/scale/edition/update/issueDate/agency/bbox per cell, +/// DSID fields reflecting the applied update chain. See tile57.h. +export fn tile57_enc_charts(path: ?[*:0]const u8, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const p = spanOpt(path) orelse return failWith(err, .badarg, "path must not be null"); + const c = Chart.openPath(p, null, false) catch |e| return failCtx(err, e, p); + defer c.deinit(); + const bytes = (c.chartsJson() catch |e| return fail(err, e)) orelse return OK; + return exportOut(err, o, n, bytes); +} -// Convert the C CellInput[] into a Zig chart.CellInput[] (slices into the host's -// borrowed buffers). Allocates the slice-of-slices into `a`; the engine copies -// what it keeps, so the caller frees the conversion arrays after the call. -fn toCellInputs(a: std.mem.Allocator, c_cells: []const CellInput) ?[]chart.CellInput { - const out = a.alloc(chart.CellInput, c_cells.len) catch return null; - for (c_cells, 0..) |cc, i| { - var ups: []const []const u8 = &.{}; - if (cc.updates != null and cc.update_lens != null and cc.update_count > 0) { - const arr = a.alloc([]const u8, cc.update_count) catch return null; - var k: usize = 0; - while (k < cc.update_count) : (k += 1) arr[k] = cc.updates.?[k][0..cc.update_lens.?[k]]; - ups = arr; - } - out[i] = .{ .base = cc.base[0..cc.base_len], .updates = ups, .name = spanOpt(cc.name) orelse "" }; +/// The features of the S-57 data at `path` (one cell or a whole ENC_ROOT) for the +/// comma-separated object-class acronyms `classes`, as a GeoJSON FeatureCollection +/// (lon/lat geometry; properties = {"class", ...full S-57 attribute map}). Parsed +/// without portrayal. NULL/0 out when nothing matched. See tile57.h. +export fn tile57_enc_features(path: ?[*:0]const u8, classes: ?[*:0]const u8, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const p = spanOpt(path) orelse return failWith(err, .badarg, "path must not be null"); + const cls = spanOpt(classes) orelse return failWith(err, .badarg, "classes must not be null"); + const c = Chart.openPath(p, null, false) catch |e| return failCtx(err, e, p); + defer c.deinit(); + const bytes = (c.featuresJson(cls) catch |e| return fail(err, e)) orelse return OK; + return exportOut(err, o, n, bytes); +} + +/// tile57_enc_features over in-memory base-cell bytes (no update chain). See tile57.h. +export fn tile57_enc_features_bytes(base: ?[*]const u8, len: usize, classes: ?[*:0]const u8, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const b = base orelse return failWith(err, .badarg, "base must not be null"); + if (len == 0) return failWith(err, .badarg, "len must not be zero"); + const cls = spanOpt(classes) orelse return failWith(err, .badarg, "classes must not be null"); + const charts_in = [_]chart.ChartInput{.{ .base = b[0..len] }}; + const c = Chart.openCharts(&charts_in, null, false) catch |e| return fail(err, e); + defer c.deinit(); + const bytes = (c.featuresJson(cls) catch |e| return fail(err, e)) orelse return OK; + return exportOut(err, o, n, bytes); +} + +/// Decode a CATALOG.031 exchange-set catalogue into a JSON array of its CATD +/// entries: [{"file","longName","impl","bbox"?}, ...]. NULL/0 out when the file +/// holds no CATD records. See tile57.h. +export fn tile57_enc_catalog(catalog_031: ?[*]const u8, len: usize, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const cat = catalog_031 orelse return failWith(err, .badarg, "catalog_031 must not be null"); + if (len == 0) return failWith(err, .badarg, "len must not be zero"); + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const entries = s57.parseCatalog(a, cat[0..len]) orelse return failWith(err, .parse, "malformed CATALOG.031"); + if (entries.len == 0) return OK; + var buf = std.ArrayList(u8).empty; + catalogJson(a, &buf, entries) catch |e| return fail(err, e); + const bytes = gpa.dupe(u8, buf.items) catch |e| return fail(err, e); + return exportOut(err, o, n, bytes); +} + +fn catalogJson(a: std.mem.Allocator, buf: *std.ArrayList(u8), entries: []const s57.CatalogEntry) !void { + try buf.append(a, '['); + for (entries, 0..) |e, i| { + if (i > 0) try buf.append(a, ','); + try buf.appendSlice(a, "{\"file\":"); + try jsonStr(a, buf, e.path); + try buf.appendSlice(a, ",\"longName\":"); + try jsonStr(a, buf, e.long_name); + try buf.appendSlice(a, ",\"impl\":"); + try jsonStr(a, buf, e.impl); + if (e.bbox) |b| try buf.print(a, ",\"bbox\":[{d},{d},{d},{d}]", .{ b[0], b[1], b[2], b[3] }); + try buf.append(a, '}'); + } + try buf.append(a, ']'); +} + +fn jsonStr(a: std.mem.Allocator, buf: *std.ArrayList(u8), s: []const u8) !void { + try buf.append(a, '"'); + for (s) |ch| switch (ch) { + '"' => try buf.appendSlice(a, "\\\""), + '\\' => try buf.appendSlice(a, "\\\\"), + else => if (ch < 0x20) { + try buf.print(a, "\\u{x:0>4}", .{ch}); + } else try buf.append(a, ch), + }; + try buf.append(a, '"'); +} + +/// Bake ONE cell (+ its updates, read from disk) to PMTiles bytes over its NATIVE +/// band zoom range, into *out / *out_len (free with tile57_free); NULL/0 when the +/// cell produced no tiles. See tile57.h. +export fn tile57_bake_chart_bytes(path: ?[*:0]const u8, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const p = spanOpt(path) orelse return failWith(err, .badarg, "path must not be null"); + const archive = chart.bakeChartBytes(p, null) catch |e| return failCtx(err, e, p); + if (archive) |a| return exportOut(err, o, n, a); + return OK; +} + +/// Bake `n` charts to per-chart PMTiles bytes IN PARALLEL across up to `workers` +/// threads; out_bytes[i]/out_lens[i] get cell i's archive (free each with +/// tile57_free) or NULL/0 when it produced nothing. *out_baked (NULL to ignore) = +/// the count that produced bytes. `workers` is a MEMORY bound. See tile57.h. +export fn tile57_bake_charts( + paths: ?[*]const ?[*:0]const u8, + n: usize, + workers: u32, + out_bytes: ?[*]?[*]u8, + out_lens: ?[*]usize, + out_baked: ?*usize, + err: ?*CError, +) callconv(.c) c_int { + if (out_baked) |p| p.* = 0; + const ps = paths orelse return failWith(err, .badarg, "paths must not be null"); + const ob = out_bytes orelse return failWith(err, .badarg, "out_bytes must not be null"); + const ol = out_lens orelse return failWith(err, .badarg, "out_lens must not be null"); + if (n == 0) return OK; + for (0..n) |i| { + ob[i] = null; + ol[i] = 0; + } + const list = gpa.alloc([]const u8, n) catch |e| return fail(err, e); + defer gpa.free(list); + for (0..n) |i| list[i] = spanOpt(ps[i]) orelse return failWith(err, .badarg, "a path in paths is null"); + const results = gpa.alloc(?[]u8, n) catch |e| return fail(err, e); + defer gpa.free(results); + chart.bakeChartsParallel(list, null, workers, results); + var baked: usize = 0; + var oom = false; + for (0..n) |i| { + const b = results[i] orelse continue; + defer chart.freeBytes(b); + if (oom) continue; + const p = exportAlloc(b.len) orelse { + oom = true; + continue; + }; + @memcpy(p[0..b.len], b); + ob[i] = p; + ol[i] = b.len; + baked += 1; } - return out; -} - -/// Open ONE S-57 cell (a .000 file, with its .001.. update chain auto-read from the -/// 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; -} - -/// 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; + if (oom) { + for (0..n) |i| { + if (ob[i]) |p| tile57_free(p); + ob[i] = null; + ol[i] = 0; + } + return failWith(err, .nomem, "out of memory"); } - return 0; + if (out_baked) |p| p.* = baked; + return OK; } -/// 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; +/// Walk `in_dir` for S-57 base charts (*.000) and bake each IN PARALLEL to the SAME +/// relative path under `out_dir` with a .pmtiles extension (+ an .sha sidecar), +/// creating subdirs as needed. INCREMENTAL: an archive already at least as new as +/// its whole input is skipped, so *out_baked (NULL to ignore) counts THIS run only. +/// An unreadable `in_dir` errors. See tile57.h. +export fn tile57_bake_tree( + in_dir: ?[*:0]const u8, + out_dir: ?[*:0]const u8, + workers: u32, + progress: chart.BakeProgress, + progress_ctx: ?*anyopaque, + out_baked: ?*u32, + err: ?*CError, +) callconv(.c) c_int { + if (out_baked) |p| p.* = 0; + const in_d = spanOpt(in_dir) orelse return failWith(err, .badarg, "in_dir must not be null"); + const out_d = spanOpt(out_dir) orelse return failWith(err, .badarg, "out_dir must not be null"); + // Stand up a threaded std.Io for the tree walk + the workers' file writes. + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + const baked = chart.bakeTree(threaded.io(), in_d, out_d, null, workers, progress, progress_ctx) catch |e| return failCtx(err, e, in_d); + if (out_baked) |p| p.* = @intCast(baked); + return OK; } -/// 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(); +/// The metadata JSON blob of a PMTiles archive (decompressed) — e.g. the embedded +/// per-cell "coverage" a single-cell bake carries — into *out / *out_len (free with +/// tile57_free); NULL/0 when the archive carries none. See tile57.h. +export fn tile57_pmtiles_metadata(pmtiles_ptr: ?[*]const u8, len: usize, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const p = pmtiles_ptr orelse return failWith(err, .badarg, "pmtiles must not be null"); + if (len == 0) return failWith(err, .badarg, "len must not be zero"); + const meta = chart.pmtilesMetadata(gpa, p[0..len]) catch |e| return fail(err, e); + if (meta) |m| return exportOut(err, o, n, m); + return OK; } -/// 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; - const cells = [_]chart.CellInput{.{ .base = base[0..len] }}; - return Chart.openCells(&cells, null, true) catch null; +/// Bake the ownership-partition DEBUG tiles from an ENC_ROOT into a single PMTiles +/// at out_path (composited ownership faces, no portrayed content — for a +/// partition-debug UI). OK with *out_cell_count = 0 and no file written when +/// nothing is covered. See tile57.h. +export fn tile57_bake_partition_debug( + enc_root: ?[*:0]const u8, + out_path: ?[*:0]const u8, + minzoom: u8, + maxzoom: u8, + band: i8, + out_cell_count: ?*u32, + err: ?*CError, +) callconv(.c) c_int { + if (out_cell_count) |p| p.* = 0; + const root = spanOpt(enc_root) orelse return failWith(err, .badarg, "enc_root must not be null"); + const outp = spanOpt(out_path) orelse return failWith(err, .badarg, "out_path must not be null"); + // The debug bake does filesystem I/O (read ENC_ROOT, write the pmtiles); the lib + // has no std.process.Init, so stand up a threaded std.Io for the call. It streams + // internally (StreamWriter over gpa), so pass the real gpa, not a scratch arena. + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + const nc = bundle.bakePartitionDebug(threaded.io(), gpa, root, outp, minzoom, maxzoom, band) catch |e| { + if (e == error.NoGeometry) return OK; // nothing covered: count stays 0 + return failCtx(err, e, root); + }; + if (out_cell_count) |p| p.* = @intCast(nc); + return OK; } -/// Open a baked PMTiles bundle from a file path. See tile57.h. -export fn tile57_chart_open_pmtiles(path: ?[*:0]const u8) callconv(.c) ?*Chart { - const p = spanOpt(path) orelse return null; +// =========================================================================== +// 4. Render — the `tile57` chart handle (see tile57.h) +// =========================================================================== + +/// Open a baked PMTiles archive from a file path, mmap'd (never fully resident; +/// the file must stay in place while the chart is open). See tile57.h. +export fn tile57_chart_open(path: ?[*:0]const u8, out: ?*?*Chart, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = null; + const p = spanOpt(path) orelse return failWith(err, .badarg, "path must not be null"); var threaded: std.Io.Threaded = .init(gpa, .{}); defer threaded.deinit(); - const io = threaded.io(); - const dir_path = std.fs.path.dirname(p) orelse "."; - var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{}) catch return null; - defer dir.close(io); - const bytes = dir.readFileAlloc(io, std.fs.path.basename(p), gpa, .unlimited) catch return null; - defer gpa.free(bytes); - return Chart.openBytes(bytes, .pmtiles, null) catch null; + o.* = chart.openPmtilesPath(threaded.io(), p) catch |e| return failCtx(err, e, p); + return OK; +} + +/// Open a baked PMTiles archive from in-memory bytes (copied). See tile57.h. +export fn tile57_chart_open_bytes(pmtiles_ptr: ?[*]const u8, len: usize, out: ?*?*Chart, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = null; + const b = pmtiles_ptr orelse return failWith(err, .badarg, "pmtiles must not be null"); + if (len == 0) return failWith(err, .badarg, "len must not be zero"); + o.* = Chart.openBytes(b[0..len], .pmtiles, null) catch |e| return fail(err, e); + return OK; } -// Fixed-size chart metadata (mirrors tile57_chart_info in tile57.h): folds the old -// zoom_range / bounds / anchor / bands getters into one struct fill. -const CChartInfo = extern struct { +// Fixed-size chart metadata (mirrors tile57_info in tile57.h). +const CInfo = extern struct { min_zoom: u8, max_zoom: u8, bands: u32, @@ -157,11 +436,8 @@ const CChartInfo = extern struct { anchor_lat: f64, anchor_lon: f64, anchor_zoom: f64, - // The encoding tile57_chart_tile returns (TILE57_TILE_TYPE_*): a PMTiles - // 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 + tile_type: u8, // the archive's stored encoding (TILE57_TILE_TYPE_*) + native_scale: i32, // embedded compilation scale (1:N); 0 = derive from zoom band }; // tile57_tile_type values (keep in sync with tile57.h). @@ -169,44 +445,53 @@ const TILE_TYPE_MVT: u8 = 1; const TILE_TYPE_MLT: u8 = 2; /// Fill *out with the chart's fixed metadata (zoom range, bands, bounds, anchor, -/// tile encoding). See tile57.h. -export fn tile57_chart_get_info(src: ?*Chart, out: *CChartInfo) callconv(.c) void { - out.* = std.mem.zeroes(CChartInfo); +/// tile encoding, embedded compilation scale). See tile57.h. +export fn tile57_chart_get_info(src: ?*Chart, out: ?*CInfo) callconv(.c) void { + const o = out orelse return; + o.* = std.mem.zeroes(CInfo); const s = src orelse return; 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()) { + o.min_zoom = zr.min; + o.max_zoom = zr.max; + o.native_scale = s.nativeScale(); + o.bands = s.bands(); + o.tile_type = switch (s.tileType()) { .mlt => TILE_TYPE_MLT, else => TILE_TYPE_MVT, }; if (s.bounds()) |b| { - out.has_bounds = true; - out.west = b[0]; - out.south = b[1]; - out.east = b[2]; - out.north = b[3]; + o.has_bounds = true; + o.west = b[0]; + o.south = b[1]; + o.east = b[2]; + o.north = b[3]; } if (s.anchor()) |a| { - out.has_anchor = true; - out.anchor_lat = a.lat; - out.anchor_lon = a.lon; - out.anchor_zoom = a.zoom; + o.has_anchor = true; + o.anchor_lat = a.lat; + o.anchor_lon = a.lon; + o.anchor_zoom = a.zoom; } } -const CQueryCb = @import("render").query.QueryCb; - -/// 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, zoom, cbp) catch return -1; - return 0; +/// The distinct SCAMIN denominators present in the chart (ascending, from the +/// archive metadata); NULL/0 out when there are none. Free *out with +/// tile57_free. See tile57.h. +export fn tile57_chart_scamin(handle: ?*Chart, out: ?*?[*]i32, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, bad_out); + const n = out_len orelse return failWith(err, .badarg, bad_out); + o.* = null; + n.* = 0; + const s = handle orelse return failWith(err, .badarg, "chart must not be null"); + const vals = s.scamin() catch |e| return fail(err, e); + defer chart.freeBytes(std.mem.sliceAsBytes(vals)); + if (vals.len == 0) return OK; + // SCAMIN denominators fit in int32 (the engine caps them). + const p = exportAlloc(vals.len * @sizeOf(i32)) orelse return failWith(err, .nomem, "out of memory"); + @memcpy(p[0 .. vals.len * @sizeOf(u32)], std.mem.sliceAsBytes(vals)); + o.* = @ptrCast(@alignCast(p)); + n.* = vals.len; + return OK; } const CCoverageCb = extern struct { @@ -214,14 +499,14 @@ const CCoverageCb = extern struct { 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; +/// The chart's M_COVR data-coverage polygons, from the coverage the bake embedded +/// in the archive metadata: cb->ring is called once per polygon with its exterior +/// ring as interleaved lon,lat doubles. OK with no calls when the archive embeds +/// none. See tile57.h. +export fn tile57_chart_coverage(handle: ?*Chart, cb: ?*const CCoverageCb, err: ?*CError) callconv(.c) c_int { + const self = handle orelse return failWith(err, .badarg, "chart must not be null"); + const cbp = cb orelse return failWith(err, .badarg, "cb must not be null"); + const polys = self.coverage() orelse return OK; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); @@ -229,177 +514,136 @@ export fn tile57_chart_coverage(handle: ?*Chart, cb: ?*const CCoverageCb) callco 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; + const flat = 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; + flat[2 * i] = @as(f64, @floatFromInt(p.lon_e7)) / 1e7; + flat[2 * i + 1] = @as(f64, @floatFromInt(p.lat_e7)) / 1e7; } - cbp.ring(cbp.ctx, out.ptr, ring.len); + cbp.ring(cbp.ctx, flat.ptr, ring.len); } - return 0; + return OK; } -// 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; +/// The chart's own stored tile at (z,x,y), decompressed (MLT or MVT per +/// tile57_info.tile_type), with NO composition — the per-archive primitive an +/// embedder's own compositor consumes. NULL/0 out when the archive has no tile +/// there. See tile57.h. +export fn tile57_chart_tile(handle: ?*Chart, z: u8, x: u32, y: u32, out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + const rd = c.pmtilesReader() orelse return failWith(err, .badarg, "chart is not archive-backed"); + const bytes = (rd.getTile(gpa, z, x, y) catch |e| return fail(err, e)) orelse return OK; + return exportOut(err, o, n, bytes); +} -// Shared bake options. Mirrors tile57_bake_opts in tile57.h. catalog_dir/created -// are read only by tile57_bake_bundle. -const CBakeOpts = extern struct { - rules_dir: ?[*:0]const u8, - catalog_dir: ?[*:0]const u8, - created: ?[*:0]const u8, - minzoom: u8, - maxzoom: u8, - omit_pick_attrs: bool, - progress: BakeProgress, - progress_user: ?*anyopaque, - // Baked tile encoding: 0 = engine default (MLT), TILE57_TILE_TYPE_MVT, - // TILE57_TILE_TYPE_MLT. Appended for ABI-append-safety (a zero-initialised - // struct bakes the default). - format: u8, -}; +const CQueryCb = @import("render").query.QueryCb; -// The passed opts or all-defaults (matching NULL opts = every field at its default). -fn bakeOptsOr(opts: ?*const CBakeOpts) CBakeOpts { - return if (opts) |p| p.* else .{ - .rules_dir = null, - .catalog_dir = null, - .created = null, - .minzoom = 0, - .maxzoom = 0, - .omit_pick_attrs = false, - .progress = null, - .progress_user = null, - .format = 0, - }; +/// 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. See tile57.h. +export fn tile57_chart_query(handle: ?*Chart, lon: f64, lat: f64, zoom: f64, cb: ?*const CQueryCb, err: ?*CError) callconv(.c) c_int { + const self = handle orelse return failWith(err, .badarg, "chart must not be null"); + const cbp = cb orelse return failWith(err, .badarg, "cb must not be null"); + self.queryPoint(lon, lat, zoom, cbp) catch |e| return fail(err, e); + return OK; } -// tile57_bake_opts.format -> engine TileFormat (0 = default = MLT). -fn bakeFormat(v: u8) @import("scene").TileFormat { - return if (v == TILE_TYPE_MVT) .mvt else .mlt; +const RenderPalette = @import("render").resolve.PaletteId; + +fn paletteOf(settings: *const mariner.Settings) RenderPalette { + return switch (settings.scheme) { + .day => .day, + .dusk => .dusk, + .night => .night, + }; } -/// Bake an ENC_ROOT into ONE PMTiles archive. See tile57.h. 1=ok, 0=empty, -1=error. -export fn tile57_bake_pmtiles( - cells_ptr: [*]const CellInput, - count: usize, - opts: ?*const CBakeOpts, - out: *[*]u8, - out_len: *usize, -) callconv(.c) c_int { - const o = bakeOptsOr(opts); - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const cells = toCellInputs(arena.allocator(), cells_ptr[0..count]) orelse return -1; - const archive = chart.bakeArchive(cells, spanOpt(o.rules_dir), o.minzoom, o.maxzoom, bakeFormat(o.format), !o.omit_pick_attrs, o.progress, o.progress_user) catch return -1; - if (archive) |a| { - out.* = a.ptr; - out_len.* = a.len; - return 1; - } - return 0; -} - -// Manifest generator string for bake_bundle (matches the CLI's "tile57 0.1.0"). -const generator = "tile57 " ++ version_string; - -/// Bake a single cell.000 OR a whole ENC_ROOT directory (on-disk paths) into a -/// self-contained chart bundle written under out_dir — the SAME package the -/// `tile57 bake … -o out/` CLI emits (tiles/chart.pmtiles with scamin+vector_layers -/// metadata + assets/{colortables,linestyles}.json + sprite-mln + per-scheme -/// style-{day,dusk,night}.json + manifest.json). rules_dir/catalog_dir NULL or "" -/// use the catalogue embedded in the library. created NULL/"" leaves the manifest -/// "created" unset (else an ISO8601 stamp). progress may be NULL (the built-in -/// console progress is used). out_cell_count / out_bbox (w,s,e,n) are optional. -/// 1=ok, 0=nothing covered (no geometry), -1=error. See tile57.h. -export fn tile57_bake_bundle( - input: [*:0]const u8, - out_dir: [*:0]const u8, - opts: ?*const CBakeOpts, - out_cell_count: ?*u32, - out_bbox: ?*[4]f64, +// Shared render prologue: width/height must be 1..MAX_RENDER_PX per side. +const MAX_RENDER_PX = 16384; +const bad_size = "width/height must be 1..16384"; + +/// Render a VIEW of this ONE chart (centre + fractional zoom + pixel size) to +/// PNG: the archive's baked tiles replayed through the native S-52 pixel path — +/// one scene across every covering tile, labels decluttered over the whole +/// canvas. No composition (see tile57_compose_png for the composed twin). +/// `m` NULL = defaults. See tile57.h. +export fn tile57_chart_png( + handle: ?*Chart, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, ) callconv(.c) c_int { - const o = bakeOptsOr(opts); - // The bundle pipeline does filesystem I/O (read ENC, write the bundle dir); the - // lib has no std.process.Init, so stand up a threaded std.Io for the call. - var threaded: std.Io.Threaded = .init(gpa, .{}); - defer threaded.deinit(); - const io = threaded.io(); - // Scratch arena: bakeBundle's allocations (paths, manifest, styles, cell names) - // are all consumed by the on-disk writes, so they're freed when this returns. - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const res = bundle.bakeBundle(io, arena.allocator(), .{ - .input = std.mem.span(input), - .out_dir = std.mem.span(out_dir), - .rules_dir = spanOpt(o.rules_dir) orelse "", - .catalog_dir = spanOpt(o.catalog_dir) orelse "", - .generator = generator, - .created = spanOpt(o.created) orelse "", - .minzoom = o.minzoom, - .maxzoom = o.maxzoom, - .format = bakeFormat(o.format), - .pick_attrs = !o.omit_pick_attrs, - .progress = o.progress, - .progress_user = o.progress_user, - }) catch |err| return if (err == error.NoGeometry) 0 else -1; - if (out_cell_count) |p| p.* = @intCast(res.cell_count); - if (out_bbox) |p| p.* = res.bounds; - return 1; -} - -/// Release a chart and all cached tiles. See tile57.h. -export fn tile57_chart_close(handle: ?*Chart) callconv(.c) void { - if (handle) |s| s.deinit(); + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = c.renderView(lon, lat, zoom, width, height, paletteOf(&settings), &settings, .png, null) catch |e| return fail(err, e); + return exportOut(err, o, n, bytes); } -/// The distinct SCAMIN denominators present in the chart (the live SCAMIN manifest; -/// see tile57.h). On success returns 1 with *out pointing at *out_len int32 values -/// (ascending), 0 if there are none; -1 on error. Free *out with tile57_free -/// ((uint8_t*)*out, *out_len * sizeof(int32_t)). -export fn tile57_chart_scamin(handle: ?*Chart, out: *[*]i32, out_len: *usize) callconv(.c) c_int { - const s = handle orelse return -1; - const vals = s.scamin() catch return -1; - if (vals.len == 0) { - chart.freeBytes(@as([*]u8, @ptrCast(vals.ptr))[0 .. vals.len * @sizeOf(u32)]); - out_len.* = 0; - return 0; - } - out.* = @ptrCast(vals.ptr); // SCAMIN denominators fit in int32 (max ~2^31) - out_len.* = vals.len; - return 1; +/// tile57_chart_png's vector twin: the SAME scene as a deterministic single-page PDF +/// (1 px = 1 pt, 72 dpi; vector fills, native strokes, glyph-outline text). +export fn tile57_chart_pdf( + handle: ?*Chart, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, +) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = c.renderView(lon, lat, zoom, width, height, paletteOf(&settings), &settings, .pdf, null) catch |e| return fail(err, e); + return exportOut(err, o, n, bytes); } -/// Fetch tile (z,x,y) as decompressed vector-tile bytes in the chart's tile -/// encoding (chart_info.tile_type: stored type for a PMTiles backend, the live -/// generation format for a cell backend). 1=OK + out/out_len, 0=empty, -1=error. -export fn tile57_chart_tile( +const CbCanvas = @import("render").cb_canvas.CCanvas; + +/// tile57_chart_png's callback twin: the SAME view painted through the C callback +/// table `canvas` (see tile57.h) instead of rasterising. Geometry in canvas +/// PIXEL space (y down), paint order, palette-resolved colours. +export fn tile57_chart_canvas( handle: ?*Chart, - z: u8, - x: u32, - y: u32, - out: *[*]u8, - out_len: *usize, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + canvas: ?*const CbCanvas, + err: ?*CError, ) callconv(.c) c_int { - const s = handle orelse return -1; - const r = s.tile(z, x, y) catch return -1; - if (r) |bytes| { - out.* = bytes.ptr; - out_len.* = bytes.len; - return 1; - } - return 0; + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + const cb = canvas orelse return failWith(err, .badarg, "canvas must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = c.renderView(lon, lat, zoom, width, height, paletteOf(&settings), &settings, .callback, cb) catch |e| return fail(err, e); + chart.freeBytes(bytes); // the callback path returns an empty buffer + return OK; } -/// Render a VIEW of the chart (centre + fractional zoom + pixel size) to PNG -/// through the native S-52 pixel path: the mariner settings evaluate LIVE -/// (real safety contour, category/SCAMIN/text-group gates, palette), symbols -/// replay as vectors, labels declutter over the whole canvas. `m` NULL = -/// defaults. Returns 0 with *out/*out_len set (free with tile57_free); -/// -1 bad handle, -2 render failure, -3 unsupported source (a baked PMTiles -/// chart carries no portrayal to render from). -export fn tile57_chart_render_view( +const CSurface = @import("render").vector.CSurface; + +/// The GPU vector twin: the SAME view emitted as 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). Pan/zoom re-portray nothing on the host. +export fn tile57_chart_surface( handle: ?*Chart, lon: f64, lat: f64, @@ -407,140 +651,193 @@ export fn tile57_chart_render_view( width: u32, height: u32, m: ?*const CMariner, - out: *[*]u8, - out_len: *usize, + surface: ?*const CSurface, + err: ?*CError, ) callconv(.c) c_int { - const c = handle orelse return -1; - 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, .png, null) catch |e| switch (e) { - error.Unsupported => return -3, - else => return -2, - }; - out.* = bytes.ptr; - out_len.* = bytes.len; - return 0; + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + const sfc = surface orelse return failWith(err, .badarg, "surface must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + c.renderSurfaceView(lon, lat, zoom, width, height, paletteOf(&settings), &settings, sfc) catch |e| return fail(err, e); + return OK; } -const RenderPalette = @import("render").resolve.PaletteId; +/// Portray ONE tile (z, x, y) to a surface — the per-tile twin of +/// tile57_chart_surface. Same WORLD-SPACE tagged draw calls, for a single tile, so +/// a host can portray+tessellate each tile once, cache it, and compose the view +/// from cached tiles (the MapLibre model). Decluttering is per-tile. See tile57.h. +export fn tile57_chart_tile_surface( + handle: ?*Chart, + z: u8, + x: u32, + y: u32, + m: ?*const CMariner, + surface: ?*const CSurface, + err: ?*CError, +) callconv(.c) c_int { + const c = handle orelse return failWith(err, .badarg, "chart must not be null"); + const sfc = surface orelse return failWith(err, .badarg, "surface must not be null"); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + c.renderSurfaceTile(z, x, y, paletteOf(&settings), &settings, sfc) catch |e| return fail(err, e); + return OK; +} -/// The chart's per-cell metadata as JSON: -/// [{"name","scale","edition","update","issueDate","agency","bbox"?}, …]. -/// DSID fields reflect the applied update chain. Returns 1 with *out/*out_len -/// set (free with tile57_free); 0 when the chart has no cells (e.g. a PMTiles -/// chart — its manifest is the sidecar); -1 on error/bad handle. -export fn tile57_chart_cells(handle: ?*Chart, out: *[*]u8, out_len: *usize) callconv(.c) c_int { - const c = handle orelse return -1; - const bytes = (c.cellsJson() catch return -1) orelse return 0; - out.* = bytes.ptr; - out_len.* = bytes.len; - return 1; +/// Release a chart and all cached tiles. Must not be called while any borrower +/// (a compositor, a renderer) may still read from it. See tile57.h. +export fn tile57_chart_close(handle: ?*Chart) callconv(.c) void { + if (handle) |s| s.deinit(); } -/// Decode a CATALOG.031 exchange-set catalogue into a JSON array of its CATD -/// entries: -/// [{"file","longName","impl","bbox"?}, …]. Not chart-scoped. Returns 1 with -/// *out/*out_len set (free with tile57_free); 0 when no CATD records; -1 on -/// parse error. -export fn tile57_catalog_entries(catalog_031: [*]const u8, len: usize, out: *[*]u8, out_len: *usize) callconv(.c) c_int { - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const a = arena.allocator(); - const entries = s57.parseCatalog(a, catalog_031[0..len]) orelse return -1; - if (entries.len == 0) return 0; - var buf = std.ArrayList(u8).empty; - buf.append(a, '[') catch return -1; - for (entries, 0..) |e, i| { - if (i > 0) buf.append(a, ',') catch return -1; - buf.appendSlice(a, "{\"file\":") catch return -1; - jsonStr(a, &buf, e.path) catch return -1; - buf.appendSlice(a, ",\"longName\":") catch return -1; - jsonStr(a, &buf, e.long_name) catch return -1; - buf.appendSlice(a, ",\"impl\":") catch return -1; - jsonStr(a, &buf, e.impl) catch return -1; - if (e.bbox) |b| buf.print(a, ",\"bbox\":[{d},{d},{d},{d}]", .{ b[0], b[1], b[2], b[3] }) catch return -1; - buf.append(a, '}') catch return -1; +// =========================================================================== +// 5. Compose — the runtime compositor over open charts (see tile57.h) +// =========================================================================== + +/// Coverage/zoom summary of a compositor, filled by tile57_compose_get_meta. +const CComposeMeta = extern struct { + min_zoom: u8, + max_zoom: u8, // deepest zoom that can be served (native windows + one fill-up overscale zoom) + charts: u32, // coverage-carrying charts held + west: f64, + south: f64, + east: f64, + north: f64, +}; + +/// Read a partition sidecar file into a fresh gpa-owned buffer (or error). Used only during open. +fn readSidecar(io: std.Io, path: []const u8) ![]u8 { + var f = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer f.close(io); + const st = try f.stat(io); + const n: usize = @intCast(st.size); + const buf = try gpa.alloc(u8, n); + errdefer gpa.free(buf); + _ = try f.readPositionalAll(io, buf, 0); + return buf; +} + +/// Open a compositor over `n` open charts, BORROWING their archives + embedded +/// coverage (the charts must outlive it; close the compositor first). Charts whose +/// archives embed no coverage are skipped; none at all is TILE57_ERR_UNSUPPORTED. +/// `partition_path` (or NULL) names a partition sidecar (tile57_compose_save_partition; +/// the `tile57 bake` CLI writes partition.tpart) to load and skip the build — a +/// missing/stale one falls back to building. See tile57.h. +export fn tile57_compose_open( + charts: ?[*]const ?*Chart, + n: usize, + partition_path: ?[*:0]const u8, + out: ?*?*compose.ComposeSource, + err: ?*CError, +) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = null; + const cs = charts orelse return failWith(err, .badarg, "charts must not be null"); + if (n == 0) return failWith(err, .badarg, "n must not be zero"); + + const archives = gpa.alloc(compose.ChartArchive, n) catch |e| return fail(err, e); + defer gpa.free(archives); + var na: usize = 0; + for (0..n) |i| { + const c = cs[i] orelse return failWith(err, .badarg, "a chart in charts is null"); + const rd = c.pmtilesReader() orelse return failWith(err, .badarg, "a chart in charts is not archive-backed"); + const cov = c.decodedCoverage() orelse continue; // embeds no coverage: owns no ground + archives[na] = .{ .reader = rd, .cov = cov }; + na += 1; + } + + // Optional partition sidecar; the lib has no std.process.Init, so stand up a + // threaded std.Io for the read (nothing else here does file I/O). + var owned: ?[]u8 = null; + defer if (owned) |b| gpa.free(b); + if (spanOpt(partition_path)) |pp| { + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + if (readSidecar(threaded.io(), pp)) |b| { + owned = b; + } else |_| {} } - buf.append(a, ']') catch return -1; - const bytes = gpa.dupe(u8, buf.items) catch return -1; - out.* = bytes.ptr; - out_len.* = bytes.len; - return 1; + + o.* = (compose.ComposeSource.open(gpa, archives[0..na], owned) catch |e| return fail(err, e)) orelse + return failWith(err, .unsupported, "no chart carries per-cell coverage"); + return OK; } -fn jsonStr(a: std.mem.Allocator, buf: *std.ArrayList(u8), s: []const u8) !void { - try buf.append(a, '"'); - for (s) |ch| switch (ch) { - '"' => try buf.appendSlice(a, "\\\""), - '\\' => try buf.appendSlice(a, "\\\\"), - else => if (ch < 0x20) { - try buf.print(a, "\\u{x:0>4}", .{ch}); - } else try buf.append(a, ch), - }; - try buf.append(a, '"'); +/// Compose the tile (z,x,y) on demand into RAW (decompressed) MLT in *out / *out_len +/// (free with tile57_free) — the HTTP layer gzips on the wire. NULL/0 out with OK = +/// no bytes; *out_owned (NULL to ignore) then distinguishes true empty ocean (false, +/// safe to cache) from owned-but-empty (true — transient during a bake, suspect +/// after). Byte-faithful to the batch compositor. See tile57.h. +export fn tile57_compose_tile( + handle: ?*compose.ComposeSource, + z: u8, + x: u32, + y: u32, + out: ?*?[*]u8, + out_len: ?*usize, + out_owned: ?*bool, + err: ?*CError, +) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + if (out_owned) |p| p.* = false; + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + const res = src.tile(gpa, z, x, y) catch |e| return fail(err, e); + if (out_owned) |p| p.* = res.owned; + if (res.tile) |t| return exportOut(err, o, n, t); + return OK; } -/// The chart's features for the given comma-separated object-class acronyms -/// (e.g. "DEPARE,DRGARE") as a GeoJSON FeatureCollection: -/// lon/lat geometry, properties = {"class", …the full S-57 attribute map}. -/// Parsed without portrayal; a whole-ENC_ROOT query walks every cell — the -/// caller owns that cost. Returns 1 with *out/*out_len set (free with -/// tile57_free); 0 when nothing matched; -1 on error. -export fn tile57_chart_features(handle: ?*Chart, classes: ?[*:0]const u8, out: *[*]u8, out_len: *usize) callconv(.c) c_int { - const c = handle orelse return -1; - const cls = classes orelse return -1; - const bytes = (c.featuresJson(std.mem.span(cls)) catch return -1) orelse return 0; - out.* = bytes.ptr; - out_len.* = bytes.len; - return 1; -} - -/// tile57_chart_render_view's vector twin: the SAME scene emitted as a -/// deterministic single-page PDF (1 px = 1 pt, 72 dpi page; vector fills, -/// native strokes, glyph-outline text). Same returns/ownership. -export fn tile57_chart_render_pdf( - handle: ?*Chart, +/// Render a VIEW over the compositor to PNG — the composed twin of tile57_chart_png: +/// every covering tile is composed on demand (seams stitched through the +/// ownership partition) and replayed through the native S-52 pixel path. See +/// tile57.h. +export fn tile57_compose_png( + handle: ?*compose.ComposeSource, lon: f64, lat: f64, zoom: f64, width: u32, height: u32, m: ?*const CMariner, - out: *[*]u8, - out_len: *usize, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, ) callconv(.c) c_int { - const c = handle orelse return -1; - 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, .pdf, null) catch |e| switch (e) { - error.Unsupported => return -3, - else => return -2, - }; - out.* = bytes.ptr; - out_len.* = bytes.len; - return 0; + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = chart.renderComposeView(src, lon, lat, zoom, width, height, paletteOf(&settings), &settings, .png, null) catch |e| return fail(err, e); + return exportOut(err, o, n, bytes); } -const CbCanvas = @import("render").cb_canvas.CCanvas; +/// tile57_compose_png's vector twin: the SAME composed scene as a deterministic +/// single-page PDF. See tile57.h. +export fn tile57_compose_pdf( + handle: ?*compose.ComposeSource, + lon: f64, + lat: f64, + zoom: f64, + width: u32, + height: u32, + m: ?*const CMariner, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, +) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = chart.renderComposeView(src, lon, lat, zoom, width, height, paletteOf(&settings), &settings, .pdf, null) catch |e| return fail(err, e); + return exportOut(err, o, n, bytes); +} -/// 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, +/// tile57_compose_png's callback twin: the SAME composed view painted through the +/// C callback table `canvas` (pixel space, paint order). See tile57.h. +export fn tile57_compose_canvas( + handle: ?*compose.ComposeSource, lon: f64, lat: f64, zoom: f64, @@ -548,35 +845,23 @@ export fn tile57_chart_render_view_cb( height: u32, m: ?*const CMariner, canvas: ?*const CbCanvas, + err: ?*CError, ) 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, - }; + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + const cb = canvas orelse return failWith(err, .badarg, "canvas must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + const bytes = chart.renderComposeView(src, lon, lat, zoom, width, height, paletteOf(&settings), &settings, .callback, cb) catch |e| return fail(err, e); chart.freeBytes(bytes); // the callback path returns an empty buffer - return 0; + return OK; } -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, +/// The composed GPU vector twin: the SAME composed view emitted as a WORLD-SPACE +/// tagged stream to the C surface callback (see tile57.h). See tile57_chart_surface for +/// the single-chart form. +export fn tile57_compose_surface( + handle: ?*compose.ComposeSource, lon: f64, lat: f64, zoom: f64, @@ -584,49 +869,65 @@ export fn tile57_chart_render_surface_cb( height: u32, m: ?*const CMariner, surface: ?*const CSurface, + err: ?*CError, ) 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; + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + const sfc = surface orelse return failWith(err, .badarg, "surface must not be null"); + if (width == 0 or height == 0 or width > MAX_RENDER_PX or height > MAX_RENDER_PX) + return failWith(err, .badarg, bad_size); + const settings: mariner.Settings = if (m) |p| marinerFromC(p) else .{}; + chart.renderComposeSurfaceView(src, lon, lat, zoom, width, height, paletteOf(&settings), &settings, sfc) catch |e| return fail(err, e); + return OK; } -/// 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 { - const p = ptr orelse return; - chart.freeBytes(@as([*]u8, @ptrCast(p))[0..len]); +/// Cursor object-query over the composed set (the S-52 pick, seams included): +/// invokes cb->feature once per displayed feature the point falls in. See +/// tile57.h. +export fn tile57_compose_query(handle: ?*compose.ComposeSource, lon: f64, lat: f64, zoom: f64, cb: ?*const CQueryCb, err: ?*CError) callconv(.c) c_int { + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + const cbp = cb orelse return failWith(err, .badarg, "cb must not be null"); + chart.composeQueryPoint(src, lon, lat, zoom, cbp) catch |e| return fail(err, e); + return OK; } -/// Drop the in-memory tile cache (bounds memory in long-running hosts). -export fn tile57_chart_clear_cache(handle: ?*Chart) callconv(.c) void { - if (handle) |s| s.clearCache(); +/// Fill *out with the compositor's zoom range + union coverage bounds (zeroed when +/// the handle is NULL). See tile57.h. +export fn tile57_compose_get_meta(handle: ?*compose.ComposeSource, out: ?*CComposeMeta) callconv(.c) void { + const o = out orelse return; + o.* = std.mem.zeroes(CComposeMeta); + const src = handle orelse return; + o.* = .{ + .min_zoom = src.minz, + .max_zoom = src.loop_max, + .charts = @intCast(src.readers.len), + .west = src.bounds[0], + .south = src.bounds[1], + .east = src.bounds[2], + .north = src.bounds[3], + }; } -/// Select the encoding for LIVE-generated tiles on a cell-backed chart -/// (0 = engine default (MLT), TILE57_TILE_TYPE_MVT, TILE57_TILE_TYPE_MLT). -/// No-op for a baked PMTiles chart — its stored encoding is fixed. Changing the -/// format drops the tile cache so served coordinates regenerate. The result is -/// reported by chart_info.tile_type. See tile57.h. -export fn tile57_chart_set_tile_format(handle: ?*Chart, fmt: u8) callconv(.c) void { - if (handle) |s| s.setTileFormat(bakeFormat(fmt)); +/// Serialize the compositor's ownership partition to the file `path` (a sidecar a +/// later tile57_compose_open can load to skip the build). See tile57.h. +export fn tile57_compose_save_partition(handle: ?*compose.ComposeSource, path: ?[*:0]const u8, err: ?*CError) callconv(.c) c_int { + const src = handle orelse return failWith(err, .badarg, "compose handle must not be null"); + const p = spanOpt(path) orelse return failWith(err, .badarg, "path must not be null"); + const bytes = src.serializePartition(gpa) catch |e| return fail(err, e); + defer gpa.free(bytes); + var threaded: std.Io.Threaded = .init(gpa, .{}); + defer threaded.deinit(); + std.Io.Dir.cwd().writeFile(threaded.io(), .{ .sub_path = p, .data = bytes }) catch |e| return failCtx(err, e, p); + return OK; } -// ---- portrayal asset generation (in-memory; mirrors tile57.h) -------------- -// -// Generate the S-101 portrayal assets from the library's embedded catalogue (or an -// on-disk PortrayalCatalog override). Reuses the same bundle emitters bake_bundle -// writes to disk, so the in-memory bundle matches the on-disk one byte-for-byte. +/// Release a compositor. Its charts stay open (and stay the caller's to close). +export fn tile57_compose_close(handle: ?*compose.ComposeSource) callconv(.c) void { + if (handle) |src| src.deinit(); +} + +// =========================================================================== +// 6. Style + portrayal assets (see tile57.h) +// =========================================================================== // The S-52 colour profile baked into the library, or null if (somehow) absent. fn embeddedColorProfileXml() ?[]const u8 { @@ -635,14 +936,12 @@ fn embeddedColorProfileXml() ?[]const u8 { } /// S-52 colortables.json from the colour profile baked into the library — no -/// on-disk catalogue needed. Pair with tile57_style_template / tile57_build_style. -/// 1=ok + out/out_len (free with tile57_free), 0=error. -export fn tile57_colortables_default(out: *[*]u8, out_len: *usize) callconv(.c) c_int { - const xml = embeddedColorProfileXml() orelse return 0; - const json = assets.colorTablesJson(gpa, xml) catch return 0; - out.* = json.ptr; - out_len.* = json.len; - return 1; +/// on-disk catalogue needed. Pair with tile57_style_template / tile57_style_build. +export fn tile57_colortables_default(out: ?*?[*]u8, out_len: ?*usize, err: ?*CError) callconv(.c) c_int { + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const xml = embeddedColorProfileXml() orelse return failWith(err, .internal, "embedded colour profile missing"); + const json = style.colorTablesJson(gpa, xml) catch |e| return fail(err, e); + return exportOut(err, o, n, json); } // All portrayal assets in memory. Mirrors tile57_assets in tile57.h; each non-null @@ -680,11 +979,12 @@ fn fillAssets(out: *CAssets, ct: []const u8, ls: []const u8, spr_json: []const u out.pattern_png_len = pat_png.len; } -/// All portrayal assets in memory (the same files bake_bundle writes to disk), from -/// the embedded catalogue (catalog_dir NULL/"") or an on-disk one. 1=ok + *out filled -/// (free with tile57_assets_free), 0=error. See tile57.h. -export fn tile57_bake_assets(catalog_dir: ?[*:0]const u8, out: *CAssets) callconv(.c) c_int { - out.* = .{}; +/// All portrayal assets in memory (the same files the offline bake writes to disk), +/// from the embedded catalogue (catalog_dir NULL/"") or an on-disk one. Free with +/// tile57_assets_free. See tile57.h. +export fn tile57_bake_assets(catalog_dir: ?[*:0]const u8, out: ?*CAssets, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = .{}; // The bundle emitters do filesystem I/O for an on-disk catalogue; the lib has no // std.process.Init, so stand up a threaded std.Io for the call. var threaded: std.Io.Threaded = .init(gpa, .{}); @@ -696,24 +996,24 @@ export fn tile57_bake_assets(catalog_dir: ?[*:0]const u8, out: *CAssets) callcon const a = arena.allocator(); const cd = spanOpt(catalog_dir) orelse ""; - const ct = bundle.colorTablesBytes(io, a, cd) catch return 0; - const ls = bundle.linestylesBytes(io, a, cd) catch return 0; - const spr = bundle.spriteAtlasBytes(io, a, cd, bundle.DEFAULT_CSS) catch return 0; - const pat = bundle.patternAtlasBytes(io, a, cd, bundle.DEFAULT_CSS) catch return 0; + const ct = bundle.colorTablesBytes(io, a, cd) catch |e| return fail(err, e); + const ls = bundle.linestylesBytes(io, a, cd) catch |e| return fail(err, e); + const spr = bundle.spriteAtlasBytes(io, a, cd, bundle.DEFAULT_CSS) catch |e| return fail(err, e); + const pat = bundle.patternAtlasBytes(io, a, cd, bundle.DEFAULT_CSS) catch |e| return fail(err, e); - fillAssets(out, ct, ls, spr.json, spr.png, pat.json, pat.png) catch { - tile57_assets_free(out); - return 0; + fillAssets(o, ct, ls, spr.json, spr.png, pat.json, pat.png) catch |e| { + tile57_assets_free(o); + return fail(err, e); }; - return 1; + return OK; } /// 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.* = .{}; +/// atlas (pivot-centred cells + {name:{x,y,width,height,pixelRatio}} JSON). Only +/// sprite_json/sprite_png are filled. Free with tile57_assets_free. See tile57.h. +export fn tile57_bake_sprite_mln(catalog_dir: ?[*:0]const u8, out: ?*CAssets, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = .{}; var threaded: std.Io.Threaded = .init(gpa, .{}); defer threaded.deinit(); const io = threaded.io(); @@ -721,12 +1021,12 @@ export fn tile57_bake_sprite_mln(catalog_dir: ?[*:0]const u8, out: *CAssets) cal 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; + const spr = bundle.spriteMlnBytes(io, a, cd, bundle.DEFAULT_CSS, &[_][]const u8{}) catch |e| return fail(err, e); + fillAssets(o, "", "", spr.json, spr.png, "", "") catch |e| { + tile57_assets_free(o); + return fail(err, e); }; - return 1; + return OK; } const glyph_sdf = @import("sprite").glyph; @@ -749,39 +1049,42 @@ fn glyphMetricsJson(a: std.mem.Allocator, atlas: *const glyph_sdf.Atlas) ![]u8 { /// 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.* = .{}; +/// Only sprite_* filled. Free with tile57_assets_free. See tile57.h. +export fn tile57_bake_glyph_sdf(out: ?*CAssets, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = .{}; 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; + const cps = glyph_sdf.defaultCodepoints(a) catch |e| return fail(err, e); + var atlas = glyph_sdf.build(a, font, cps, 32.0, 6) catch |e| return fail(err, e); + const png = (atlas.encodePng(a) catch |e| return fail(err, e)) orelse + return failWith(err, .internal, "glyph atlas PNG encode produced nothing"); + const json = glyphMetricsJson(a, &atlas) catch |e| return fail(err, e); + o.sprite_png = (gpa.dupe(u8, png) catch |e| { + tile57_assets_free(o); + return fail(err, e); }).ptr; - out.sprite_png_len = png.len; - out.sprite_json = (gpa.dupe(u8, json) catch { - tile57_assets_free(out); - return 0; + o.sprite_png_len = png.len; + o.sprite_json = (gpa.dupe(u8, json) catch |e| { + tile57_assets_free(o); + return fail(err, e); }).ptr; - out.sprite_json_len = json.len; - return 1; + o.sprite_json_len = json.len; + return OK; } /// 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]); - if (out.linestyles) |p| chart.freeBytes(p[0..out.linestyles_len]); - if (out.sprite_json) |p| chart.freeBytes(p[0..out.sprite_json_len]); - if (out.sprite_png) |p| chart.freeBytes(p[0..out.sprite_png_len]); - if (out.pattern_json) |p| chart.freeBytes(p[0..out.pattern_json_len]); - if (out.pattern_png) |p| chart.freeBytes(p[0..out.pattern_png_len]); - out.* = .{}; +export fn tile57_assets_free(out: ?*CAssets) callconv(.c) void { + const o = out orelse return; + if (o.colortables) |p| chart.freeBytes(p[0..o.colortables_len]); + if (o.linestyles) |p| chart.freeBytes(p[0..o.linestyles_len]); + if (o.sprite_json) |p| chart.freeBytes(p[0..o.sprite_json_len]); + if (o.sprite_png) |p| chart.freeBytes(p[0..o.sprite_png_len]); + if (o.pattern_json) |p| chart.freeBytes(p[0..o.pattern_json_len]); + if (o.pattern_png) |p| chart.freeBytes(p[0..o.pattern_png_len]); + o.* = .{}; } // ---- chart-style generation (mirrors tile57_mariner in tile57.h) ----------- @@ -814,11 +1117,11 @@ const CMariner = extern struct { size_scale: f64, // S-52 §14.5 fine-grained viewing-group control: a DENY-LIST of the raw `vg` // ids the mariner turned OFF (NULL/len 0 -> every group shown). Appended at the - // end for ABI-append-safety. The pointee must outlive the tile57_build_style call. + // end for ABI-append-safety. The pointee must outlive the tile57_style_build call. viewing_groups_off: [*c]const i32, viewing_groups_off_len: u32, - // scamin-layers.md: gate SCAMIN with a live client filter instead of per-value - // bucket layers (one *_scamin layer per render-type). Appended for ABI-append-safety. + // Gate SCAMIN with a live client filter instead of per-value bucket layers + // (one *_scamin layer per render-type). Appended for ABI-append-safety. scamin_filter_gate: bool, // S-52 §10.1.10 overscale indication (AP(OVERSC01) over overscaled coverage): // drives the `overscale` layer's visibility. Appended for ABI-append-safety; @@ -832,11 +1135,11 @@ fn dateViewSlice(buf: *const [9]u8) []const u8 { return buf[0..@min(n, 8)]; } -// Translate the extern CMariner into the internal chartstyle.MarinerSettings the +// Translate the extern CMariner into the internal mariner.Settings the // style builders take. The returned value borrows `cm`'s date_view and // viewing_groups_off storage, so `cm` (and its viewing_groups_off array) must // outlive every use of the result — true within a single ABI call. -fn marinerFromC(cm: *const CMariner) chartstyle.MarinerSettings { +fn marinerFromC(cm: *const CMariner) mariner.Settings { return .{ .scheme = switch (cm.scheme) { 1 => .dusk, @@ -883,16 +1186,19 @@ fn scaminBuf(scamin: ?[*]const i32, scamin_count: usize) ![]u32 { const p = scamin orelse return &.{}; if (scamin_count == 0) return &.{}; const buf = try gpa.alloc(u32, scamin_count); - for (p[0..scamin_count], 0..) |v, i| buf[i] = @intCast(v); + // SCAMIN is a 1:N denominator (> 0); a negative is garbage from the host. Clamp + // to 0 ("no scale gate") rather than let a safety-checked @intCast abort the + // whole process across the ABI. + for (p[0..scamin_count], 0..) |v, i| buf[i] = if (v < 0) 0 else @intCast(v); return buf; } /// Build a MapLibre style JSON from a template + mariner settings + colortables. -/// 1=ok + out/out_len (free with tile57_free), 0=error. -export fn tile57_build_style( - template_json: [*]const u8, +/// See tile57.h. +export fn tile57_style_build( + template_json: ?[*]const u8, template_len: usize, - cm: *const CMariner, + cm: ?*const CMariner, colortables_json: ?[*]const u8, colortables_len: usize, enabled_bands: ?[*]const i32, @@ -900,42 +1206,36 @@ export fn tile57_build_style( scamin: ?[*]const i32, scamin_count: usize, scamin_lat: f64, - out: *[*]u8, - out_len: *usize, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, ) callconv(.c) c_int { - const m = marinerFromC(cm); - const tmpl = template_json[0..template_len]; + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const tp = template_json orelse return failWith(err, .badarg, "template_json must not be null"); + const cmp = cm orelse return failWith(err, .badarg, "mariner must not be null"); + const m = marinerFromC(cmp); + const tmpl = tp[0..template_len]; const cts: []const u8 = if (colortables_json) |p| p[0..colortables_len] else ""; const bands: ?[]const i32 = if (enabled_bands) |p| p[0..enabled_band_count] else null; // SCAMIN manifest (the distinct denominators the host read from the source / // TileJSON): converted from the host's i32 to the u32 denominators styleJson - // buckets on. Empty/NULL -> the *_scamin layers stay ungated (host §"Still - // needed" #1: the runtime build_style now emits the SAME per-value native-minzoom - // buckets the offline bundle does when given the manifest). - const scamin_buf = scaminBuf(scamin, scamin_count) catch return 0; + // buckets on. Empty/NULL -> the *_scamin layers stay ungated. + const scamin_buf = scaminBuf(scamin, scamin_count) catch |e| return fail(err, e); defer if (scamin_buf.len > 0) gpa.free(scamin_buf); const now_unix: i64 = @intCast(time(null)); - // Single style builder: regenerate the full style with the mariner baked in - // (chartstyle.buildStyle's template-patch pass is retired). buildFromTemplate lifts - // the source config out of the passed template and drives the one styleJson. - const style = assets.buildFromTemplateScamin(gpa, tmpl, &m, cts, bands, now_unix, scamin_buf, scamin_lat) catch return 0; - out.* = style.ptr; - out_len.* = style.len; - return 1; -} - -/// Compute the minimal MapLibre style-mutation ops to turn the style for `old_m` -/// into the style for `new_m` (same template/colortables/bands/scamin inputs as -/// tile57_build_style, so the two styles are comparable). Writes a JSON op array to -/// out/out_len (free with tile57_free): "[]" when nothing changed, one op per -/// differing filter/paint/layout key, or [{"op":"rebuild"}] when the two mariners -/// would produce a different SET of layers (host falls back to a full setStyle). -/// 1=ok, 0=error. See style-diff.md. + const style_json = style.buildFromTemplateScamin(gpa, tmpl, &m, cts, bands, now_unix, scamin_buf, scamin_lat) catch |e| return fail(err, e); + return exportOut(err, o, n, style_json); +} + +/// Compute the minimal MapLibre style-mutation ops turning the style for `old_m` +/// into the style for `new_m` (same inputs as tile57_style_build, so the styles are +/// comparable): a JSON op array — "[]" when nothing changed, [{"op":"rebuild"}] +/// when the layer SET differs (host falls back to setStyle). See tile57.h. export fn tile57_style_diff( - template_json: [*]const u8, + template_json: ?[*]const u8, template_len: usize, - old_m: *const CMariner, - new_m: *const CMariner, + old_m: ?*const CMariner, + new_m: ?*const CMariner, colortables_json: ?[*]const u8, colortables_len: usize, enabled_bands: ?[*]const i32, @@ -943,45 +1243,39 @@ export fn tile57_style_diff( scamin: ?[*]const i32, scamin_count: usize, scamin_lat: f64, - out: *[*]u8, - out_len: *usize, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, ) callconv(.c) c_int { - const om = marinerFromC(old_m); - const nm = marinerFromC(new_m); - const tmpl = template_json[0..template_len]; + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const tp = template_json orelse return failWith(err, .badarg, "template_json must not be null"); + const omp = old_m orelse return failWith(err, .badarg, "old_m must not be null"); + const nmp = new_m orelse return failWith(err, .badarg, "new_m must not be null"); + const om = marinerFromC(omp); + const nm = marinerFromC(nmp); + const tmpl = tp[0..template_len]; const cts: []const u8 = if (colortables_json) |p| p[0..colortables_len] else ""; const bands: ?[]const i32 = if (enabled_bands) |p| p[0..enabled_band_count] else null; - const scamin_buf = scaminBuf(scamin, scamin_count) catch return 0; + const scamin_buf = scaminBuf(scamin, scamin_count) catch |e| return fail(err, e); defer if (scamin_buf.len > 0) gpa.free(scamin_buf); // One wall-clock read shared by both builds so "today" date resolution matches // on both sides — otherwise a clock tick could show as a spurious date-filter op. const now_unix: i64 = @intCast(time(null)); - const old_style = assets.buildFromTemplateScamin(gpa, tmpl, &om, cts, bands, now_unix, scamin_buf, scamin_lat) catch return 0; + const old_style = style.buildFromTemplateScamin(gpa, tmpl, &om, cts, bands, now_unix, scamin_buf, scamin_lat) catch |e| return fail(err, e); defer gpa.free(old_style); - const new_style = assets.buildFromTemplateScamin(gpa, tmpl, &nm, cts, bands, now_unix, scamin_buf, scamin_lat) catch return 0; + const new_style = style.buildFromTemplateScamin(gpa, tmpl, &nm, cts, bands, now_unix, scamin_buf, scamin_lat) catch |e| return fail(err, e); defer gpa.free(new_style); - const ops = assets.styleDiff(gpa, old_style, new_style) catch return 0; - out.* = ops.ptr; - out_len.* = ops.len; - return 1; + const ops = style.diff(gpa, old_style, new_style) catch |e| return fail(err, e); + return exportOut(err, o, n, ops); } /// Generate the base MapLibre style template from the catalogue baked into the -/// library — no on-disk catalogue or template file needed. This carries the chart -/// `sources` block, sprite/glyph URLs and the layer set; mariner settings are then -/// applied on top with tile57_build_style (which substitutes only paint/filter -/// props and takes no source). `scheme` is a tile57_scheme. `source_tiles` is the -/// {z}/{x}/{y} chart tiles URL (NULL -> a default pmtiles:// source). `sprite` / -/// `glyphs` are base URLs that enable the symbol / text layers (NULL omits them). -/// `minzoom` is the chart source's tile floor, emitted VERBATIM — pass the -/// archive's real minzoom (0 = tiles exist from z0; MapLibre never requests below -/// a source's minzoom, so an inflated floor blanks every lower zoom). `maxzoom` -/// of 0 -> engine default. `tile_encoding` is the chart source's tile encoding -/// (a tile57_tile_type; TILE57_TILE_TYPE_MLT emits `"encoding":"mlt"` on the -/// source so maplibre-gl >=5.12 decodes MLT natively; 0/MVT emits nothing). -/// 1=ok + out/out_len (free with tile57_free), 0=error. +/// library — the chart `sources` block, sprite/glyph URLs and the layer set; +/// mariner settings are then applied on top with tile57_style_build. See tile57.h +/// for the parameter semantics (minzoom emitted verbatim; tile_encoding MLT emits +/// "encoding":"mlt" on the source). export fn tile57_style_template( scheme: c_int, source_tiles: ?[*:0]const u8, @@ -990,13 +1284,15 @@ export fn tile57_style_template( minzoom: u32, maxzoom: u32, tile_encoding: u8, - out: *[*]u8, - out_len: *usize, + out: ?*?[*]u8, + out_len: ?*usize, + err: ?*CError, ) callconv(.c) c_int { - const xml = embeddedColorProfileXml() orelse return 0; - const cts = assets.colorTablesJson(gpa, xml) catch return 0; + const o, const n = bytesOut(out, out_len) catch return failWith(err, .badarg, bad_out); + const xml = embeddedColorProfileXml() orelse return failWith(err, .internal, "embedded colour profile missing"); + const cts = style.colorTablesJson(gpa, xml) catch |e| return fail(err, e); defer gpa.free(cts); - var opts = assets.StyleOpts{ + var opts = style.Options{ .scheme = switch (scheme) { 1 => "dusk", 2 => "night", @@ -1010,16 +1306,15 @@ export fn tile57_style_template( opts.minzoom = minzoom; if (maxzoom != 0) opts.maxzoom = maxzoom; if (tile_encoding == TILE_TYPE_MLT) opts.encoding = "mlt"; - const style = assets.styleJson(gpa, opts) catch return 0; - out.* = style.ptr; - out_len.* = style.len; - return 1; + const style_json = style.json(gpa, opts) catch |e| return fail(err, e); + return exportOut(err, o, n, style_json); } /// Fill `cm` with the canonical default mariner settings. date_view = "". -export fn tile57_mariner_defaults(cm: *CMariner) callconv(.c) void { - const d = chartstyle.MarinerSettings{}; - cm.* = .{ +export fn tile57_mariner_defaults(cm: ?*CMariner) callconv(.c) void { + const o = cm orelse return; + const d = mariner.Settings{}; + o.* = .{ .scheme = @intCast(@intFromEnum(d.scheme)), .shallow_contour = d.shallow_contour, .safety_contour = d.safety_contour, @@ -1051,3 +1346,24 @@ export fn tile57_mariner_defaults(cm: *CMariner) callconv(.c) void { .show_overscale = d.show_overscale, }; } + +// =========================================================================== +// 7. Util (see tile57.h) +// =========================================================================== + +/// Populate the process-global read-only registries (S-100 catalogue + linestyles) on +/// the calling thread. Call ONCE on the main thread before opening/baking charts from +/// worker threads, so concurrent bake/render is race-free. See tile57.h. +export fn tile57_warmup() callconv(.c) void { + chart.warmup(); +} + +/// Free any engine-returned buffer (tiles, style, the scamin array, colortables, +/// …). Buffers are length-prefixed at allocation, so the pointer is all it +/// needs — the universal free. See tile57.h. +export fn tile57_free(ptr: ?*anyopaque) callconv(.c) void { + const p = ptr orelse return; + const base: [*]align(16) u8 = @alignCast(@as([*]u8, @ptrCast(p)) - EXPORT_HDR); + const total = std.mem.readInt(usize, base[0..@sizeOf(usize)], .little); + gpa.free(base[0..total]); +} diff --git a/src/chart.zig b/src/chart.zig index 1f53a32..50702ff 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -3,32 +3,37 @@ //! cells) and it serves decompressed Mapbox Vector Tiles by (z, x, y). Multi-cell //! ENC_ROOT sources index cells cheaply and parse + portray them lazily per //! requested tile (LRU-bounded), so a host can open the whole NOAA catalogue -//! instantly and pay only for the cells under the current view. `bakeArchive` -//! bakes an ENC_ROOT into one band-streamed PMTiles archive. +//! instantly and pay only for the cells under the current view. Baking is +//! strictly per-cell: each cell to its own PMTiles archive. //! //! This is the single source of truth; the C ABI (capi.zig / include/tile57.h) //! is a thin shim over these types. The engine uses a single thread-safe -//! general-purpose allocator internally — `tile`/`bakeArchive` return bytes owned -//! by it; free them with `freeBytes`. +//! general-purpose allocator internally — the render / bake / JSON entry points +//! return bytes owned by it; free them with `freeBytes`. //! -//! Threading: a Chart is NOT internally synchronized — don't call `tile` on the -//! same Chart from multiple threads concurrently (the tile cache + LRU mutate -//! without a lock). Distinct charts are independent. `openCells`/`bakeArchive` -//! parallelize internally over cores. +//! Threading: a Chart is NOT internally synchronized — don't call its render / +//! query methods on the same Chart from multiple threads concurrently. Distinct +//! charts are independent. `openCharts`/`bakeChartsParallel` parallelize +//! internally over cores. const std = @import("std"); const pmtiles = @import("tiles").pmtiles; +const filemap = @import("tiles").filemap; +const mlt = @import("tiles").mlt; +const tiles_mvt = @import("tiles").mvt; const gzip = @import("tiles").gzip; const s57 = @import("s57"); const scene = @import("scene"); const portray = @import("portray"); const bake_enc = @import("scene").bake_enc; -const catalogue = @import("s100").catalogue; +const catalogue = @import("s101").catalogue; const tile = @import("tiles").tile; const render = @import("render"); const sprite = @import("sprite"); const embedded_assets = @import("catalog"); // S-101 portrayal assets (renderView store) -const assets = @import("assets"); // displayDenomZ (the physical display-scale formula) +const style = @import("style"); // displayDenomZ (the physical display-scale formula) +const cell_coverage = @import("coverage"); // per-cell M_COVR coverage embedded in archive metadata +const compose_mod = @import("compose"); // the runtime compositor (compose-backed view renders) // smp_allocator (Zig's fast thread-safe GPA), not page_allocator: the engine // makes many small, short-lived allocations (tile cache, cell dupes, index @@ -40,16 +45,16 @@ const gpa = std.heap.smp_allocator; extern fn tg_env_rules() callconv(.c) ?[*:0]const u8; /// Backend / on-disk format. `auto` sniffs PMTiles first, then S-57. -pub const Format = enum { auto, pmtiles, s57_cell }; +pub const Format = enum { auto, pmtiles, s57 }; /// One ENC cell: the base .000 bytes plus its sequential update files (.001…). /// Bytes are borrowed for the duration of the call (copied where retained). -pub const CellInput = struct { +pub const ChartInput = struct { base: []const u8, updates: []const []const u8 = &.{}, /// Source ENC cell name (dataset stem, e.g. "US4MD81M") for the pick report's /// "source cell" badge. "" = unknown (the `cell` prop is omitted). The eager - /// (openCells) path copies it; the bake path borrows it for the call. + /// (openCharts) path copies it; the bake path borrows it for the call. name: []const u8 = "", }; @@ -63,7 +68,7 @@ pub const Progress = ?*const fn (user: ?*anyopaque, stage: u8, done: usize, tota /// Pre-peeked metadata for one cell in a streaming open: its geographic extent /// and compilation scale (1:cscl). The host supplies these (cheap to compute, or /// already known) so the source opens without reading any cell bytes. -pub const CellMeta = extern struct { +pub const ChartMeta = extern struct { west: f64, south: f64, east: f64, @@ -75,7 +80,7 @@ pub const CellMeta = extern struct { /// malloc-allocated buffers (base + each update); the engine frees them with /// libc free() once the cell is parsed. update arrays are parallel, length /// update_count (0 / null for a base-only cell). -pub const CellBytes = extern struct { +pub const ChartBytes = extern struct { base: [*]const u8 = undefined, base_len: usize = 0, updates: ?[*]const [*]const u8 = null, @@ -87,9 +92,9 @@ pub const CellBytes = extern struct { /// engine frees them), returning true on success. Called on demand the first /// time a tile needs the cell (and again after the cell is LRU-evicted), so the /// host holds only the working set's bytes — not the whole ENC_ROOT. -pub const CellReadFn = *const fn (user: ?*anyopaque, index: usize, out: *CellBytes) callconv(.c) bool; +pub const ChartReadFn = *const fn (user: ?*anyopaque, index: usize, out: *ChartBytes) callconv(.c) bool; -/// Free bytes returned by `Chart.tile` / `bakeArchive` (page-allocator owned). +/// Free bytes returned by the render / bake / JSON entry points (page-allocator owned). pub fn freeBytes(bytes: []u8) void { gpa.free(bytes); } @@ -158,7 +163,7 @@ const LazySource = struct { tick: u64 = 0, loaded: usize = 0, max_loaded: usize = 256, // LRU budget on parsed+portrayed cells (wide views) - reader: ?CellReadFn = null, // streaming: read a cell's bytes on demand + reader: ?ChartReadFn = null, // streaming: read a cell's bytes on demand reader_user: ?*anyopaque = null, // Path-backed streaming (chart-api.md): when the chart was opened from an on-disk // ENC_ROOT, this owns the retained Io + Dir + per-cell paths and is the @@ -196,7 +201,7 @@ fn bboxOverlap(a_: [4]f64, b_: [4]f64) bool { } // Free the host-malloc'd buffers a streaming reader transferred to us (libc free). -fn freeCellBytes(cb: *CellBytes) void { +fn freeCellBytes(cb: *ChartBytes) void { if (cb.base_len != 0) std.c.free(@ptrCast(@constCast(cb.base))); if (cb.updates) |ups| { var k: usize = 0; @@ -226,7 +231,7 @@ fn cdup(bytes: []const u8) ?[*]u8 { // Peek `relpath`'s bbox+scale; on success append an index-aligned meta + a gpa-owned // copy of the path. Cells that don't read / have no coverage bbox are skipped (both // lists), keeping meta[i] and paths[i] aligned with the streaming cell index. -fn addPathCell(io: std.Io, dir: std.Io.Dir, relpath: []const u8, metas: *std.ArrayList(CellMeta), paths: *std.ArrayList([]u8)) !void { +fn addPathCell(io: std.Io, dir: std.Io.Dir, relpath: []const u8, metas: *std.ArrayList(ChartMeta), paths: *std.ArrayList([]u8)) !void { const bytes = dir.readFileAlloc(io, relpath, gpa, .unlimited) catch return; defer gpa.free(bytes); const m = s57.peekMeta(gpa, bytes) orelse return; @@ -235,10 +240,10 @@ fn addPathCell(io: std.Io, dir: std.Io.Dir, relpath: []const u8, metas: *std.Arr try paths.append(gpa, try gpa.dupe(u8, relpath)); } -// Internal CellReadFn for a path-backed chart: read cell `index`'s base .000 + its +// Internal ChartReadFn for a path-backed chart: read cell `index`'s base .000 + its // sequential .001.. updates from the retained dir into libc-malloc'd buffers (the // engine frees them via freeCellBytes). Mirrors the baker's per-cell load. -fn pathRead(user: ?*anyopaque, index: usize, out: *CellBytes) callconv(.c) bool { +fn pathRead(user: ?*anyopaque, index: usize, out: *ChartBytes) callconv(.c) bool { const ctx: *PathCtx = @ptrCast(@alignCast(user orelse return false)); if (index >= ctx.paths.len) return false; const bpath = ctx.paths[index]; @@ -290,7 +295,7 @@ fn pathRead(user: ?*anyopaque, index: usize, out: *CellBytes) callconv(.c) bool // (freeing the host's originals). Returns false if the reader declines/fails. fn streamRead(ls: *LazySource, lc: *LazyCell) bool { const rd = ls.reader orelse return false; - var cb: CellBytes = .{}; + var cb: ChartBytes = .{}; if (!rd(ls.reader_user, lc.index, &cb)) return false; const base = gpa.dupe(u8, cb.base[0..cb.base_len]) catch { freeCellBytes(&cb); @@ -405,10 +410,6 @@ fn lazyFreeCell(lc: *LazyCell) void { if (lc.name.len > 0) gpa.free(@constCast(lc.name)); } -fn tileKey(z: u8, x: u32, y: u32) u64 { - return (@as(u64, z) << 48) | (@as(u64, x) << 24) | @as(u64, y); -} - // Resolve the S-101 rules dir: explicit arg, else TILE57_S101_RULES, else "" — // which uses the rules embedded in the binary (the Lua searcher in lua_shim.c), // so no on-disk catalogue is required. A non-empty path overrides the embedded @@ -433,6 +434,58 @@ fn openPmtiles(copy: []u8) ?*Chart { return null; }; src.* = .{ .backend = .{ .reader = reader }, .data = copy, .cache = std.AutoHashMap(u64, []u8).init(gpa) }; + attachEmbeddedCoverage(src); + return src; +} + +// A per-cell bake embeds the source cell's M_COVR coverage + compilation scale in +// the archive's metadata JSON; surface them on the opened chart so coverage() and +// nativeScale() answer for a baked archive exactly as they did for the live cell. +// Best-effort: an archive without (or with unparseable) coverage attaches nothing. +fn attachEmbeddedCoverage(src: *Chart) void { + const rd = &src.backend.reader; + const h = rd.header; + if (h.metadata_length == 0) return; + const raw = rd.bytes[@intCast(h.metadata_offset)..][0..@intCast(h.metadata_length)]; + const cov_arena = gpa.create(std.heap.ArenaAllocator) catch return; + cov_arena.* = std.heap.ArenaAllocator.init(gpa); + const a = cov_arena.allocator(); + const drop = struct { + fn f(ar: *std.heap.ArenaAllocator) void { + ar.deinit(); + gpa.destroy(ar); + } + }.f; + const json: []const u8 = switch (h.internal_compression) { + .none => raw, + .gzip => gzip.decompress(a, raw) catch return drop(cov_arena), + else => return drop(cov_arena), + }; + const cov = (cell_coverage.decodeFromMetadata(a, json) catch null) orelse return drop(cov_arena); + if (cov.cscl == 0 and cov.cov1.len == 0) return drop(cov_arena); + src.cell_cov = cov; + src.coverage_arena = cov_arena; +} + +/// Open a baked PMTiles archive from a file path, mmap'd rather than copied — a +/// whole chart library can be open without being resident (the page cache holds the +/// working set). The mapping is released in deinit; the file must stay in place for +/// the chart's lifetime. +pub fn openPmtilesPath(io: std.Io, path: []const u8) !*Chart { + var f = std.Io.Dir.cwd().openFile(io, path, .{}) catch return error.NotFound; + defer f.close(io); + const st = f.stat(io) catch return error.IoFailed; + const len: usize = @intCast(st.size); + if (len == 0) return error.InvalidArchive; + const map = filemap.mapReadonly(f.handle, len) catch return error.IoFailed; + errdefer filemap.unmap(map); + var reader = pmtiles.Reader.init(gpa, map) catch return error.InvalidArchive; + const src = gpa.create(Chart) catch { + reader.deinit(); + return error.OutOfMemory; + }; + src.* = .{ .backend = .{ .reader = reader }, .data_map = map, .cache = std.AutoHashMap(u64, []u8).init(gpa) }; + attachEmbeddedCoverage(src); return src; } @@ -528,77 +581,401 @@ fn readCellFiles(path: []const u8) !CellFiles { 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); +/// Bake a SINGLE .000 cell (+ updates) to a PMTiles archive over its NATIVE band's +/// zoom range (`bandZooms(bandOf(cscl))`) and nothing else — the composite model bakes +/// each cell at its own compilation scale; the stitcher combines them and handles any +/// cross-band zoom expansion. Returns the bytes (gpa-owned; free with tile57_free). +/// null = nothing baked. +/// +/// The archive metadata embeds the cell's own coverage (M_COVR + cscl + date/name), +/// so the composite stitcher rebuilds the ownership partition from the baked archives +/// without re-parsing the .000. Read it back with `decodedCoverageFromArchive`. +pub fn bakeChartBytes(cell_path: []const u8, rules_dir: ?[]const u8) !?[]u8 { + // Populate the read-only portrayal globals (feature catalogue + complex-linestyle table) + // before portraying: without them, complex lines fall back to plain geometry and their S-52 + // linestyle is dropped from the tile. Idempotent; in the parallel batch path bakeChartsParallel + // has already warmed up before spawning workers, so this is a no-op there (and race-free). + warmup(); + var cf = try readCellFiles(cell_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 }, .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 = &.{}; + // Capture coverage for the embedded sidecar (one cheap parse). The stem is the + // ownership tie-break name — matches the coverage loader. + var cov_arena = std.heap.ArenaAllocator.init(gpa); + defer cov_arena.deinit(); + var coverage_json: ?[]const u8 = null; + var cscl: i32 = s57.peekScale(gpa, cf.base) orelse 0; if (s57.parseCellWithUpdates(gpa, cf.base, cf.updates)) |parsed| { var cell = parsed; - coverage = cell.mcovrCoverage(cov_arena.allocator()); // assembled into cov_arena - cell.deinit(); + defer cell.deinit(); + cscl = cell.params.cscl; + const stem = std.fs.path.stem(std.fs.path.basename(cell_path)); + const band: u8 = @intFromEnum(bake_enc.bandOf(cscl)); + const cc = scene.coverage.fromCell(cov_arena.allocator(), &cell, stem, band); + coverage_json = scene.coverage.encodeJson(cov_arena.allocator(), cc) catch null; } 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; + // The cell's band window, plus the extend_min fill DOWN to z0: sub-band tiles + // (scamin-thinned by the scene cull) let the compositor pull this cell up into + // coarser zooms where nothing coarser covers — a harbor-only region still + // shows land and coast at z4. No overscale above the window. + const zr = bake_enc.bandZooms(bake_enc.bandOf(cscl)); + const cell_in = [_]ChartInput{.{ .base = cf.base, .updates = cf.updates }}; + return bakeArchive(&cell_in, resolveRulesDir(rules_dir), 0, zr.max, .mlt, true, null, null, coverage_json); } -/// 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 batch cell-bake ------------------------------------------------- +// Bake many cells to their own per-cell PMTiles concurrently. The engine returns BYTES only — it +// never touches an output directory; the host writes each archive into the cache it manages. Each +// concurrent bake holds a whole cell's parse + portray + raster working set, so `workers` is a +// MEMORY bound (keep it small), not a core count. + +// MAX_BAKE_WORKERS is a hard ceiling on batch-bake threads; the host normally passes far fewer. +const MAX_BAKE_WORKERS = 32; + +const BakeCtx = struct { + next: std.atomic.Value(usize), + paths: []const []const u8, + rules_dir: ?[]const u8, + out: []?[]u8, +}; + +fn bakeCellWorker(ctx: *BakeCtx) void { + // One cell per thread. Tile generation is serial (bake_enc.serialFor), so a worker is exactly + // one thread — W workers stay W threads, never W x cpus. + while (true) { + const i = ctx.next.fetchAdd(1, .monotonic); + if (i >= ctx.paths.len) return; + ctx.out[i] = bakeChartBytes(ctx.paths[i], ctx.rules_dir) catch null; + } } -/// 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); +/// Bake each cell in `paths` (a .000 path; its .001.. updates auto-read) to its own native-scale +/// PMTiles bytes IN PARALLEL across up to `workers` threads, writing cell i's archive to out[i] +/// (caller owns it — free each with freeBytes) or leaving it null when that cell produced nothing +/// or failed. out.len must equal paths.len. Race-free: warms up the process globals first, then +/// each bakeChartBytes is independent (thread-safe allocator, thread-local portrayal context). +/// `workers` is clamped to [1, min(paths.len, MAX_BAKE_WORKERS)] and is a MEMORY bound. +pub fn bakeChartsParallel(paths: []const []const u8, rules_dir: ?[]const u8, workers: usize, out: []?[]u8) void { + std.debug.assert(out.len == paths.len); + for (out) |*o| o.* = null; + if (paths.len == 0) return; + warmup(); // idempotent — populate the read-only globals before any worker touches them + var ctx = BakeCtx{ .next = std.atomic.Value(usize).init(0), .paths = paths, .rules_dir = rules_dir, .out = out }; + var n = @min(@max(workers, 1), paths.len); + if (n > MAX_BAKE_WORKERS) n = MAX_BAKE_WORKERS; + if (n <= 1) return bakeCellWorker(&ctx); + var threads: [MAX_BAKE_WORKERS]std.Thread = undefined; + var spawned: usize = 0; + while (spawned < n - 1) : (spawned += 1) { + threads[spawned] = std.Thread.spawn(.{}, bakeCellWorker, .{&ctx}) catch break; + } + bakeCellWorker(&ctx); // this thread participates too + for (threads[0..spawned]) |t| t.join(); +} + +// ---- parallel batch cell-bake TO FILES (the host-cache path) ------------------- +// Same parallel bake, but the engine WRITES each cell's PMTiles to a caller-provided path and +// frees it right after — so a host never holds N archives (peak memory ~ the worker count). The +// APP owns the cache: it names every out_path, so distinct library consumers don't clash. A +// .sha content-hash sidecar is written beside each archive for the host's cache token. + +/// Progress callback: invoked with (ctx, done, total) after each cell is processed, so a host can +/// drive an import progress bar. It may be called CONCURRENTLY from worker threads (done arrives +/// monotonically per fetch but can be delivered slightly out of order), so the callback must be +/// thread-safe. Null to skip. +pub const BakeProgress = ?*const fn (?*anyopaque, u32, u32) callconv(.c) void; + +const BakeFileCtx = struct { + next: std.atomic.Value(usize), + in_paths: []const []const u8, + out_paths: []const []const u8, + rules_dir: ?[]const u8, + io: std.Io, + ok: []bool, + progress: BakeProgress, + progress_ctx: ?*anyopaque, + done: std.atomic.Value(u32), +}; + +fn bakeOneToFile(ctx: *BakeFileCtx, i: usize) void { + const arc = (bakeChartBytes(ctx.in_paths[i], ctx.rules_dir) catch null) orelse return; + defer freeBytes(arc); + std.Io.Dir.cwd().writeFile(ctx.io, .{ .sub_path = ctx.out_paths[i], .data = arc }) catch return; + var digest: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(arc, &digest, .{}); + const hex = std.fmt.bytesToHex(digest, .lower); + var sha_buf: [std.fs.max_path_bytes + 8]u8 = undefined; + if (std.fmt.bufPrint(&sha_buf, "{s}.sha", .{ctx.out_paths[i]})) |sha_path| { + std.Io.Dir.cwd().writeFile(ctx.io, .{ .sub_path = sha_path, .data = &hex }) catch {}; + } else |_| {} + ctx.ok[i] = true; +} + +fn bakeFileWorker(ctx: *BakeFileCtx) void { + while (true) { + const i = ctx.next.fetchAdd(1, .monotonic); + if (i >= ctx.in_paths.len) return; + bakeOneToFile(ctx, i); + const d = ctx.done.fetchAdd(1, .monotonic) + 1; // attempted count (smooth progress) + if (ctx.progress) |cb| cb(ctx.progress_ctx, d, @intCast(ctx.in_paths.len)); + } +} + +/// Bake each in_paths[i] in parallel (up to `workers` threads) and WRITE its PMTiles to +/// out_paths[i] (plus an .sha content-hash sidecar), freeing each archive right after +/// the write — so the host never holds N archives (peak memory ~ the worker count). The app owns +/// the cache and names every out_path. `progress(progress_ctx, done, total)` fires (serialised) +/// after each cell. Race-free (warms up first; each bake is independent). Returns the count written. +pub fn bakeChartsToFiles(io: std.Io, in_paths: []const []const u8, out_paths: []const []const u8, rules_dir: ?[]const u8, workers: usize, progress: BakeProgress, progress_ctx: ?*anyopaque) usize { + std.debug.assert(out_paths.len == in_paths.len); + if (in_paths.len == 0) return 0; + warmup(); + const ok = gpa.alloc(bool, in_paths.len) catch return 0; + defer gpa.free(ok); + @memset(ok, false); + var ctx = BakeFileCtx{ .next = std.atomic.Value(usize).init(0), .in_paths = in_paths, .out_paths = out_paths, .rules_dir = rules_dir, .io = io, .ok = ok, .progress = progress, .progress_ctx = progress_ctx, .done = std.atomic.Value(u32).init(0) }; + var n = @min(@max(workers, 1), in_paths.len); + if (n > MAX_BAKE_WORKERS) n = MAX_BAKE_WORKERS; + if (n <= 1) { + bakeFileWorker(&ctx); + } else { + var threads: [MAX_BAKE_WORKERS]std.Thread = undefined; + var spawned: usize = 0; + while (spawned < n - 1) : (spawned += 1) threads[spawned] = std.Thread.spawn(.{}, bakeFileWorker, .{&ctx}) catch break; + bakeFileWorker(&ctx); + for (threads[0..spawned]) |t| t.join(); + } + var count: usize = 0; + for (ok) |o| { + if (o) count += 1; + } + return count; +} + +/// Walk `in_dir` for S-57 base cells (*.000) and bake each, IN PARALLEL, to the SAME relative path +/// under `out_dir` with a .pmtiles extension (in_dir/d1/US4CT1AA.000 -> out_dir/d1/US4CT1AA.pmtiles), +/// plus an .sha sidecar. Output subdirs are created as needed. `in_dir` is the source ENC data; +/// `out_dir` is the caller's own cache (it owns the location + names, so consumers don't clash). The +/// engine writes + frees each archive, so the host never holds N in memory. `progress` fires per +/// cell (serialised) for an import progress bar. INCREMENTAL: a cell whose mirrored archive is +/// already at least as new as its whole input (.000 + update chain) is skipped, so a re-run over an +/// unchanged tree bakes nothing. Returns the count baked THIS run; errors if `in_dir` is unreadable. +pub fn bakeTree(io: std.Io, in_dir: []const u8, out_dir: []const u8, rules_dir: ?[]const u8, workers: usize, progress: BakeProgress, progress_ctx: ?*anyopaque) !usize { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + + var in_paths = std.ArrayList([]const u8).empty; + var out_paths = std.ArrayList([]const u8).empty; + + var dir = try std.Io.Dir.cwd().openDir(io, in_dir, .{ .iterate = true }); + defer dir.close(io); + var walker = try dir.walk(a); + defer walker.deinit(); + while (walker.next(io) catch null) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".000")) continue; + const in_path = std.fs.path.join(a, &.{ in_dir, entry.path }) catch continue; + // Mirror the relative path, swapping .000 -> .pmtiles. + const rel_noext = entry.path[0 .. entry.path.len - ".000".len]; + const out_rel = std.fmt.allocPrint(a, "{s}.pmtiles", .{rel_noext}) catch continue; + const out_path = std.fs.path.join(a, &.{ out_dir, out_rel }) catch continue; + // Incremental: skip a cell whose mirrored archive already exists and is at least as new as + // its whole input — the .000 AND its update chain (.001, .002, …) — so re-baking a provider + // (adding a district, or dropping a new .001 update) only re-bakes what actually changed. + if (fileModNs(io, out_path)) |out_ns| { + if (newestInputNs(io, in_path)) |in_ns| { + if (out_ns >= in_ns) continue; + } + } + if (std.fs.path.dirname(out_path)) |d| std.Io.Dir.cwd().createDirPath(io, d) catch {}; + in_paths.append(a, in_path) catch continue; + out_paths.append(a, out_path) catch continue; + } + if (in_paths.items.len == 0) return 0; + return bakeChartsToFiles(io, in_paths.items, out_paths.items, rules_dir, workers, progress, progress_ctx); +} + +/// The file's modification time in nanoseconds, or null if it doesn't exist / can't be statted. +fn fileModNs(io: std.Io, path: []const u8) ?i96 { + var f = std.Io.Dir.cwd().openFile(io, path, .{}) catch return null; + defer f.close(io); + const st = f.stat(io) catch return null; + return st.mtime.nanoseconds; +} + +/// The NEWEST mtime across a cell's whole input: its base .000 (`in_path`) and its contiguous +/// update chain .001, .002, … (stopping at the first gap — the same discovery readCellFiles +/// uses to apply them). Null if the base is missing. So a freshly-dropped .001 makes the cell newer +/// than a previously-baked archive, forcing a re-bake. +fn newestInputNs(io: std.Io, in_path: []const u8) ?i96 { + var newest = fileModNs(io, in_path) orelse return null; + const dir = std.fs.path.dirname(in_path) orelse "."; + const bn = std.fs.path.basename(in_path); + if (bn.len > 4) { + const stem = bn[0 .. bn.len - 4]; // strip ".000" + var u: u32 = 1; + while (u <= 999) : (u += 1) { + var buf: [std.fs.max_path_bytes]u8 = undefined; + const up = std.fmt.bufPrint(&buf, "{s}{s}{s}.{d:0>3}", .{ dir, std.fs.path.sep_str, stem, u }) catch break; + const ns = fileModNs(io, up) orelse break; // gap → the chain ends here + if (ns > newest) newest = ns; + } + } + return newest; +} + +// Build the embedded-catalogue symbol + area-fill store for a view render (in `a`; +// deinit when done) and register the complex-linestyle table (idempotent, +// gpa-backed — the registry outlives the render, shared with any later bake). +fn viewSymbolStore(a: std.mem.Allocator, palette: render.resolve.PaletteId) !*sprite.CatalogStore { + 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); + + var ls_srcs = std.ArrayList(style.LineStyleSrc).empty; + defer ls_srcs.deinit(gpa); + for (embedded_assets.linestyles) |e| ls_srcs.append(gpa, .{ .id = e.name, .xml = e.bytes }) catch {}; + scene.linestyle.registerLinestylesXml(gpa, ls_srcs.items); + return store; +} + +// ---- compose-backed view renders -------------------------------------------- +// +// The compositor is the tile source; these are its VIEW backends: compose every +// covering tile on demand (seams stitched through the ownership partition) and +// replay it through the native S-52 pixel path — the same scene Chart.renderView +// replays from a single archive, but across the whole composed set. + +/// Render a VIEW over a runtime compositor to PNG / PDF / a callback canvas +/// (per `output`; bytes are gpa-owned, freeBytes — empty for the callback +/// output). The mariner's live-swappable settings evaluate at render time. +pub fn renderComposeView(src: *compose_mod.ComposeSource, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, 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(); + + var colors = try render.resolve.Colors.init(a, embedded_assets.colorprofile[0].bytes); + const store = try viewSymbolStore(a, palette); + defer store.deinit(); + + const pt: f32 = @floatCast(256.0 * std.math.pow(f64, 2.0, zoom - @round(zoom))); + var ps = render.pixel.PixelSurface.initView(a, &colors, palette, settings, zoom, w, h, pt, tile.EXTENT); + ps.store = store.asStore(); + ps.output = output; + ps.cb = cb_table; + + var vt = scene.ViewTiles.init(lon, lat, zoom, w, h, pt); + const surf = ps.asSurface(); + try surf.beginScene(vt.z); + while (vt.next()) |t| { + const res = src.tile(a, t.z, t.x, t.y) catch continue; + const bytes = res.tile orelse continue; + const layers = mlt.decode(a, bytes) catch continue; // the compositor serves raw MLT + ps.setOrigin(t.origin_x, t.origin_y); + scene.replayTile(a, surf, layers) catch return error.TileGen; + } + return surf.endScene(gpa) catch error.TileGen; +} + +/// renderComposeView's GPU-vector twin: the SAME composed view emitted as a +/// WORLD-SPACE tagged stream to the C surface callback (see render/vector.zig). +pub fn renderComposeSurfaceView(src: *compose_mod.ComposeSource, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, 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 store = try viewSymbolStore(a, palette); + defer store.deinit(); + + 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 + + var vt = scene.ViewTiles.init(lon, lat, zoom, w, h, pt); + const surf = vs.asSurface(); + try surf.beginScene(vt.z); + while (vt.next()) |t| { + const res = src.tile(a, t.z, t.x, t.y) catch continue; + const bytes = res.tile orelse continue; + const layers = mlt.decode(a, bytes) catch continue; + vs.setTile(t.z, t.x, t.y); + scene.replayTile(a, surf, layers) catch continue; + } + _ = try surf.endScene(a); +} + +/// Cursor object-query over a runtime compositor (the S-52 §10.8 pick across the +/// whole composed set — seams included): replay the composed tile covering +/// (lon,lat) at the view zoom through a QuerySurface and report each feature the +/// point falls in via `cb`. +pub fn composeQueryPoint(src: *compose_mod.ComposeSource, lon: f64, lat: f64, zoom: f64, cb: *const render.query.QueryCb) !void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + // Query the tile at the VIEW zoom, clamped into the served range: its features + // are already SCAMIN-bucketed to what's displayed and the pick radius (tile + // units) maps to a constant on-screen distance. + const zc = std.math.clamp(@round(zoom), @as(f64, @floatFromInt(src.minz)), @as(f64, @floatFromInt(src.loop_max))); + const z: u8 = @intFromFloat(zc); + const world = tile.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 = tile.project(lon, lat, z, tx, ty, tile.EXTENT); + var qs = render.query.QuerySurface{ + .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(); + try surf.beginScene(z); + const res = src.tile(a, z, tx, ty) catch return; + const bytes = res.tile orelse return; + const layers = mlt.decode(a, bytes) catch return; + scene.replayTile(a, surf, layers) catch return; + _ = surf.endScene(a) catch {}; +} + +/// The metadata JSON blob of a PMTiles archive (decompressed), duped into `a`, or null +/// if the archive carries none. For a host to read the embedded scamin / coverage +/// without a full open. This engine writes metadata uncompressed; gzip is handled for +/// archives from another writer. +pub fn pmtilesMetadata(a: std.mem.Allocator, archive: []const u8) !?[]u8 { + var r = try pmtiles.Reader.init(a, archive); + defer r.deinit(); + const h = r.header; + if (h.metadata_length == 0) return null; + const raw = r.bytes[@intCast(h.metadata_offset)..][0..@intCast(h.metadata_length)]; + return switch (h.internal_compression) { + .none => try a.dupe(u8, raw), + .gzip => try gzip.decompress(a, raw), + else => null, + }; +} + +/// Decode the per-cell coverage embedded in a baked per-cell archive's metadata, or +/// null if absent. The whole result (rings + strings) is allocated in `a`. The +/// composite stitcher calls this over each cell's archive to rebuild the ownership +/// partition without re-parsing the source .000. +pub fn decodedCoverageFromArchive(a: std.mem.Allocator, archive: []const u8) !?scene.coverage.ChartCoverage { + const json = (try pmtilesMetadata(a, archive)) orelse return null; + return scene.coverage.decodeFromMetadata(a, json); } /// Populate the process-global READ-ONLY registries (the S-100 feature catalogue and @@ -609,15 +986,15 @@ pub fn bakeCellBytes(cell_path: []const u8, rules_dir: ?[]const u8, minzoom: u8, /// 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; + var ls_srcs = std.ArrayList(style.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); + scene.linestyle.registerLinestylesXml(gpa, ls_srcs.items); } // Parallel open worker: peek each cell's band + bbox and copy its bytes. const OpenWork = struct { - inputs: []const CellInput, + inputs: []const ChartInput, out: []LazyCell, ok: []bool, @@ -651,7 +1028,7 @@ const OpenWork = struct { } }; -/// The public chart handle. Open with `openBytes`/`openCells`; release with +/// The public chart handle. Open with `openBytes`/`openCharts`; release with /// `deinit`. pub const Chart = struct { backend: Backend, @@ -662,19 +1039,35 @@ pub const Chart = struct { // Defaults ON; the C ABI open can turn it off for lean tiles. (No effect on a // PMTiles/reader backend — those tiles are already baked.) pick_attrs: bool = true, - // Encoding for LIVE-generated tiles (cell backends). Defaults to MVT so - // existing chart-handle consumers (MVT-only renderers) are unaffected; a host - // whose renderer decodes MLT opts in via setTileFormat. A PMTiles/reader - // backend ignores it — stored tiles serve verbatim in their baked encoding - // (see tileType). + // The tile encoding reported for a cell backend (via tileType); fixed at MVT. + // A PMTiles/reader 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 = &.{}, + // The per-cell coverage decoded from the opened archive's metadata (name, date, + // cscl, bbox, M_COVR rings) — read back by coverage()/nativeScale(), and what a + // compositor borrows to place this chart in the ownership partition. Storage + // lives in coverage_arena (freed in deinit). + cell_cov: ?cell_coverage.ChartCoverage = null, coverage_arena: ?*std.heap.ArenaAllocator = null, - cscl_override: i32 = 0, + // A path-opened archive is mmap'd rather than copied (never fully resident); + // released in deinit. Bytes-opened archives use `data` instead. + data_map: ?[]align(std.heap.page_size_min) const u8 = null, + + // ---- view-render caches (reader backend) -------------------------------- + // A view re-render replays mostly the SAME tiles (a pan reveals one strip, a + // zoom settle swaps one band), and profiling put ~35% of every re-render in + // gzip+MLT decode and another ~3% in re-building the palette and re-parsing + // the SVG symbol catalogue. All of it is immutable for an open handle, so it + // caches here: decoded tiles in a generation-evicted map (each entry owns its + // arena, so eviction frees exactly one tile), colors + per-palette symbol + // stores in one handle-lifetime arena. Same threading rule as the handle: + // NOT synchronized. + view_tiles: std.AutoHashMapUnmanaged(u64, *DecodedTile) = .empty, + view_gen: u64 = 0, + view_tiles_max: usize = 192, // > the ~96 tiles of one 2560px view: never evicts mid-render + view_arena: ?*std.heap.ArenaAllocator = null, + view_colors: ?*render.resolve.Colors = null, + view_stores: [3]?*sprite.CatalogStore = .{ null, null, null }, /// 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 @@ -683,23 +1076,23 @@ pub const Chart = struct { if (fmt == .pmtiles or fmt == .auto) { const copy = try gpa.dupe(u8, bytes); if (openPmtiles(copy)) |src| return src; // openPmtiles freed `copy` on failure - if (fmt == .pmtiles) return error.OpenFailed; + if (fmt == .pmtiles) return error.InvalidArchive; } - return openCell(bytes, rules_dir) orelse error.OpenFailed; + return openCell(bytes, rules_dir) orelse error.InvalidCell; } /// Open an ENC_ROOT as a multi-cell source: cells are indexed cheaply (band + /// bbox) in parallel and parsed/portrayed lazily per tile. All bytes are /// copied. Errors if no cell's header parses. - pub fn openCells(cells_in: []const CellInput, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { - if (cells_in.len == 0) return error.OpenFailed; + pub fn openCharts(cells_in: []const ChartInput, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { + if (cells_in.len == 0) return error.NotFound; const dir = resolveRulesDir(rules_dir); const dir_copy = try gpa.dupe(u8, dir); errdefer gpa.free(dir_copy); - const tmp = gpa.alloc(LazyCell, cells_in.len) catch return error.OpenFailed; + const tmp = try gpa.alloc(LazyCell, cells_in.len); defer gpa.free(tmp); - const ok = gpa.alloc(bool, cells_in.len) catch return error.OpenFailed; + const ok = try gpa.alloc(bool, cells_in.len); defer gpa.free(ok); @memset(ok, false); @@ -710,11 +1103,11 @@ pub const Chart = struct { for (ok) |k| { if (k) valid += 1; } - if (valid == 0) return error.OpenFailed; + if (valid == 0) return error.InvalidCell; // cells provided, but none parsed const cells = gpa.alloc(LazyCell, valid) catch { for (tmp, ok) |*lc, k| if (k) lazyFreeCell(lc); - return error.OpenFailed; + return error.OutOfMemory; }; var j: usize = 0; for (tmp, ok) |lc, k| if (k) { @@ -725,7 +1118,7 @@ pub const Chart = struct { const src = gpa.create(Chart) catch { for (cells) |*lc| lazyFreeCell(lc); gpa.free(cells); - return error.OpenFailed; + return error.OutOfMemory; }; src.* = .{ .backend = .{ .cells = .{ .cells = cells, .rules_dir = dir_copy } }, @@ -740,12 +1133,12 @@ pub const Chart = struct { /// returns a cell's bytes on demand. Cell bytes are read only when a tile /// needs them and freed on LRU eviction, so the host holds the working set's /// bytes — not the whole ENC_ROOT. No bytes are read at open. Errors if empty. - pub fn openCellsStreaming(metas: []const CellMeta, reader: CellReadFn, user: ?*anyopaque, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { - if (metas.len == 0) return error.OpenFailed; + pub fn openChartsStreaming(metas: []const ChartMeta, reader: ChartReadFn, user: ?*anyopaque, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { + if (metas.len == 0) return error.NotFound; const dir = resolveRulesDir(rules_dir); const dir_copy = try gpa.dupe(u8, dir); errdefer gpa.free(dir_copy); - const cells = gpa.alloc(LazyCell, metas.len) catch return error.OpenFailed; + const cells = try gpa.alloc(LazyCell, metas.len); for (metas, 0..) |m, i| { cells[i] = .{ .base = &.{}, @@ -775,7 +1168,6 @@ pub const Chart = struct { /// bytes on demand for the working set (freed on LRU eviction) — the caller hands /// over only a path and the engine holds only what tiles need. The chart owns the /// retained Io + Dir for its lifetime (freed in deinit). Errors if no cell parses. - /// TODO(chart-api): unify the enumeration with the baker's bakeRoot (parity-gated). pub fn openPath(path: []const u8, rules_dir: ?[]const u8, pick_attrs: bool) !*Chart { const threaded = try gpa.create(std.Io.Threaded); errdefer gpa.destroy(threaded); @@ -788,7 +1180,7 @@ pub const Chart = struct { var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }); errdefer dir.close(io); - var metas = std.ArrayList(CellMeta).empty; + var metas = std.ArrayList(ChartMeta).empty; defer metas.deinit(gpa); var paths = std.ArrayList([]u8).empty; errdefer { @@ -820,7 +1212,7 @@ pub const Chart = struct { // Reuse the streaming backend with the internal file reader; the returned // Chart owns the PathCtx (Io + Dir + paths) via ls.path_ctx, freed in deinit. - const src = try openCellsStreaming(metas.items, pathRead, null, rules_dir, pick_attrs); + const src = try openChartsStreaming(metas.items, pathRead, null, rules_dir, pick_attrs); errdefer src.deinit(); const ctx = try gpa.create(PathCtx); errdefer gpa.destroy(ctx); @@ -831,6 +1223,97 @@ pub const Chart = struct { } /// Release the source and all cached tiles. + const DecodedTile = struct { + arena: std.heap.ArenaAllocator, + layers: []tiles_mvt.DecodedLayer, + gen: u64, + }; + + fn viewTileKey(z: u8, x: u32, y: u32) u64 { + return (@as(u64, z) << 58) | (@as(u64, x) << 29) | @as(u64, y); + } + + /// The decoded layers of stored tile (z,x,y) through the handle's decoded-tile + /// cache. Null when the archive has no tile there (not cached — the miss is a + /// directory binary-search, no decompression). Entries stay valid until this + /// handle either evicts them (never within one render; see view_tiles_max) or + /// closes. + fn viewTileLayers(self: *Chart, rd: *pmtiles.Reader, z: u8, x: u32, y: u32) ?[]tiles_mvt.DecodedLayer { + const key = viewTileKey(z, x, y); + self.view_gen += 1; + if (self.view_tiles.getPtr(key)) |ep| { + ep.*.gen = self.view_gen; + return ep.*.layers; + } + const e = gpa.create(DecodedTile) catch return null; + e.* = .{ .arena = std.heap.ArenaAllocator.init(gpa), .layers = &.{}, .gen = self.view_gen }; + const ea = e.arena.allocator(); + const is_mlt = rd.header.tile_type == .mlt; + const ok = blk: { + const bytes = (rd.getTile(ea, z, x, y) catch break :blk false) orelse break :blk false; + e.layers = (if (is_mlt) mlt.decode(ea, bytes) else tiles_mvt.decode(ea, bytes)) catch break :blk false; + break :blk true; + }; + if (!ok) { + e.arena.deinit(); + gpa.destroy(e); + return null; + } + if (self.view_tiles.count() >= self.view_tiles_max) { + // Evict the least-recently-touched entry (linear scan; the map is small). + var oldest_key: u64 = 0; + var oldest_gen: u64 = std.math.maxInt(u64); + var it = self.view_tiles.iterator(); + while (it.next()) |kv| { + if (kv.value_ptr.*.gen < oldest_gen) { + oldest_gen = kv.value_ptr.*.gen; + oldest_key = kv.key_ptr.*; + } + } + if (self.view_tiles.fetchRemove(oldest_key)) |kv| { + kv.value.arena.deinit(); + gpa.destroy(kv.value); + } + } + self.view_tiles.put(gpa, key, e) catch { + e.arena.deinit(); + gpa.destroy(e); + return null; + }; + return e.layers; + } + + /// The handle-lifetime arena backing the cached palette + symbol stores. + fn viewArena(self: *Chart) !std.mem.Allocator { + if (self.view_arena == null) { + const va = try gpa.create(std.heap.ArenaAllocator); + va.* = std.heap.ArenaAllocator.init(gpa); + self.view_arena = va; + } + return self.view_arena.?.allocator(); + } + + /// The palette colour tables, parsed once per handle (all three palettes). + fn viewColorsRef(self: *Chart) !*render.resolve.Colors { + if (self.view_colors) |c| return c; + const va = try self.viewArena(); + const c = try va.create(render.resolve.Colors); + c.* = try render.resolve.Colors.init(va, embedded_assets.colorprofile[0].bytes); + self.view_colors = c; + return c; + } + + /// The palette's symbol store, built once per handle per palette (the SVG + /// catalogue parse dominated the old per-render setup). + fn viewStoreFor(self: *Chart, palette: render.resolve.PaletteId) !*sprite.CatalogStore { + const idx: usize = @intFromEnum(palette); + if (self.view_stores[idx]) |st| return st; + const va = try self.viewArena(); + const st = try viewSymbolStore(va, palette); + self.view_stores[idx] = st; + return st; + } + pub fn deinit(self: *Chart) void { switch (self.backend) { .reader => |*r| r.deinit(), @@ -846,10 +1329,23 @@ pub const Chart = struct { while (it.next()) |v| gpa.free(v.*); self.cache.deinit(); if (self.data) |d| gpa.free(d); + if (self.data_map) |m| filemap.unmap(m); if (self.coverage_arena) |ca| { ca.deinit(); gpa.destroy(ca); } + { + var vit = self.view_tiles.valueIterator(); + while (vit.next()) |v| { + v.*.arena.deinit(); + gpa.destroy(v.*); + } + self.view_tiles.deinit(gpa); + } + if (self.view_arena) |va| { + va.deinit(); + gpa.destroy(va); + } gpa.destroy(self); } @@ -857,36 +1353,59 @@ pub const Chart = struct { pub fn format(self: *Chart) Format { return switch (self.backend) { .reader => .pmtiles, - .cell, .cells => .s57_cell, + .cell, .cells => .s57, }; } /// 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. + /// gaps to coarser cells. A live cell parses it; a per-cell baked PMTiles + /// surfaces the copy embedded in its archive metadata. Null when absent. pub fn coverage(self: *const Chart) ?[]const []const []const s57.LonLat { - if (self.coverage_override.len > 0) return self.coverage_override; + if (self.cell_cov) |cc| { + if (cc.cov1.len > 0) return cc.cov1; + } 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). + /// The cell's compilation scale (DSPM CSCL, 1:N), so the host doesn't derive an + /// over-detailed one from the 0..18 zoom range. A live cell parses it; a per-cell + /// baked PMTiles reads the copy in its archive metadata. 0 = unknown (derive from + /// the zoom band instead). pub fn nativeScale(self: *const Chart) i32 { - if (self.cscl_override != 0) return self.cscl_override; + if (self.cell_cov) |cc| { + if (cc.cscl != 0) return cc.cscl; + } 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 + /// The chart's PMTiles reader, for a compositor to borrow (null unless this is an + /// archive-backed chart). The reader lives inside the chart: the chart must outlive + /// every borrower, and a borrower's reads must not run concurrently with this + /// chart's own render/query calls (no internal lock). + pub fn pmtilesReader(self: *Chart) ?*pmtiles.Reader { + return switch (self.backend) { + .reader => |*r| r, + else => null, + }; + } + + /// The per-cell coverage embedded in the opened archive's metadata (name, date, + /// cscl, bbox, M_COVR rings), or null if the archive carries none. Borrows the + /// chart's storage. + pub fn decodedCoverage(self: *const Chart) ?cell_coverage.ChartCoverage { + return self.cell_cov; + } + + /// The tile encoding this chart's tiles carry: 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 /// reported as-is. pub fn tileType(self: *Chart) pmtiles.TileType { return switch (self.backend) { @@ -898,19 +1417,6 @@ pub const Chart = struct { }; } - /// Select the encoding for LIVE-generated tiles (cell backends; no-op for a - /// baked PMTiles backend — its stored encoding is fixed). Drops the tile - /// cache so already-served coordinates regenerate in the new format. - pub fn setTileFormat(self: *Chart, fmt: scene.TileFormat) void { - switch (self.backend) { - .reader => return, - .cell, .cells => {}, - } - if (self.tile_format == fmt) return; - self.tile_format = fmt; - self.clearCache(); - } - /// Min/max zoom served (PMTiles: archive range; cell: 0..18). pub fn zoomRange(self: *Chart) struct { min: u8, max: u8 } { return switch (self.backend) { @@ -1018,75 +1524,23 @@ pub const Chart = struct { } } - /// Fetch tile (z,x,y) as decompressed vector-tile bytes in the chart's tile - /// encoding (`tileType`): a PMTiles backend serves the stored bytes verbatim - /// (MVT or MLT, whatever was baked); a cell backend generates in - /// `tile_format`. Returns null for an empty / absent tile, else - /// page-allocator-owned bytes (free with `freeBytes`). Cached per source, so - /// re-requests are cheap and deterministic. - pub fn tile(self: *Chart, z: u8, x: u32, y: u32) !?[]u8 { - const key = tileKey(z, x, y); - if (self.cache.get(key)) |cached| { - if (cached.len == 0) return null; - return try gpa.dupe(u8, cached); - } - // Baked tiles are the ONLY tile path: cell-backed charts exist for - // metadata/inspection (cellsJson/featuresJson/scamin/renderView on a - // single cell), never on-demand tile generation — bake first, serve - // the archive. - const bytes: []u8 = switch (self.backend) { - .reader => |*r| (r.getTile(gpa, z, x, y) catch return error.TileGen) orelse try gpa.alloc(u8, 0), - .cell, .cells => return error.TileGen, - }; - if (self.cache.count() >= self.cache_max) { - var cit = self.cache.valueIterator(); - while (cit.next()) |v| gpa.free(v.*); - self.cache.clearRetainingCapacity(); - } - self.cache.put(key, bytes) catch {}; // best-effort; cache owns `bytes` on success - if (bytes.len == 0) return null; - return try gpa.dupe(u8, bytes); - } - /// Render a VIEW of the chart — centre + fractional zoom + pixel size — /// through the native S-52 pixel path: real portrayal, vector symbols, /// 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, cb_table: ?*const render.cb_canvas.CCanvas) ![]u8 { + pub fn renderView(self: *Chart, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, 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(); - 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(); - - // Complex-linestyle table (idempotent): without it MARSYS51/cable/ - // pipeline styles degrade to generic dashed strokes. gpa-backed: the - // registry outlives this render (shared with any later bake). - 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 colors = try self.viewColorsRef(); + const store = try self.viewStoreFor(palette); // Continuous scaling between integer zooms; the host applies physical // calibration / @2x via settings.size_scale. const pt: f32 = @floatCast(256.0 * std.math.pow(f64, 2.0, zoom - @round(zoom))); - var ps = render.pixel.PixelSurface.initView(a, &colors, palette, settings, zoom, w, h, pt, @import("tiles").tile.EXTENT); + 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; @@ -1100,13 +1554,8 @@ pub const Chart = struct { var vt = scene.ViewTiles.init(lon, lat, zoom, w, h, pt); const surf = ps.asSurface(); try surf.beginScene(vt.z); - 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; + const layers = self.viewTileLayers(rd, t.z, t.x, t.y) orelse continue; ps.setOrigin(t.origin_x, t.origin_y); scene.replayTile(a, surf, layers) catch return error.TileGen; } @@ -1131,50 +1580,26 @@ pub const Chart = struct { /// 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 { + pub fn renderSurfaceView(self: *Chart, lon: f64, lat: f64, zoom: f64, w: u32, h: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, 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 colors = try self.viewColorsRef(); + const store = try self.viewStoreFor(palette); 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); + 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 + 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); 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; + const layers = self.viewTileLayers(rd, t.z, t.x, t.y) orelse continue; vs.setTile(t.z, t.x, t.y); scene.replayTile(a, surf, layers) catch continue; } @@ -1199,6 +1624,52 @@ pub const Chart = struct { _ = try surf.endScene(a); } + /// Portray a SINGLE tile (z, x, y) to a CSurface — the per-tile twin of + /// renderSurfaceView. Emits the same WORLD-SPACE tagged draw calls, but for + /// exactly one tile instead of every tile under a view, so a host can portray + + /// tessellate each tile ONCE, cache the geometry, and compose tiles itself + /// (the MapLibre model). Decluttering is per-tile (labels resolve within the + /// tile), so a host that wants cross-tile label suppression must still do a + /// separate view-level text pass. `view_zoom` is the tile's own zoom. + pub fn renderSurfaceTile(self: *Chart, z: u8, x: u32, y: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, cb: *const render.vector.CSurface) !void { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + + const colors = try self.viewColorsRef(); + const store = try self.viewStoreFor(palette); + + var vs = render.vector.VectorSurface.init(a, colors, palette, settings, cb); + vs.store = store.asStore(); + vs.view_zoom = @floatFromInt(z); // declutter at the tile's native zoom + const surf = vs.asSurface(); + + try surf.beginScene(z); + switch (self.backend) { + .reader => |*rd| { + if (self.viewTileLayers(rd, z, x, y)) |layers| { + vs.setTile(z, x, y); + scene.replayTile(a, surf, layers) catch {}; + } + }, + .cell => |*cb2| { + const one = [_]scene.CellRef{.{ + .cell = &cb2.cell, + .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, + }}; + vs.setTile(z, x, y); + scene.appendTile(surf, a, &one, z, x, y, self.pick_attrs) catch {}; + }, + .cells => return error.Unsupported, + } + _ = 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. @@ -1256,7 +1727,7 @@ pub const Chart = struct { /// one Unicode character per terminal cell (cols x rows), optional /// ANSI-256 color. Returns UTF-8 bytes, one '\n'-terminated row per grid /// row (gpa-owned; free with freeBytes). Same backends as renderView. - pub fn renderAscii(self: *Chart, lon: f64, lat: f64, zoom: f64, cols: u32, rows: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.MarinerSettings, ansi: bool) ![]u8 { + pub fn renderAscii(self: *Chart, lon: f64, lat: f64, zoom: f64, cols: u32, rows: u32, palette: render.resolve.PaletteId, settings: *const render.resolve.Settings, ansi: bool) ![]u8 { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); @@ -1310,7 +1781,7 @@ pub const Chart = struct { /// geometry extent, omitted when none parses. Returns null when the chart /// has no cells (a PMTiles chart carries no cell files — its manifest is /// the host-side sidecar). gpa-owned; free with freeBytes. - pub fn cellsJson(self: *Chart) !?[]u8 { + pub fn chartsJson(self: *Chart) !?[]u8 { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); @@ -1351,7 +1822,7 @@ pub const Chart = struct { } else { // Streaming cell: read transiently (collectScaminCells pattern). const rd = ls.reader orelse continue; - var cb: CellBytes = .{}; + var cb: ChartBytes = .{}; if (!rd(ls.reader_user, lc.index, &cb)) continue; defer freeCellBytes(&cb); var ups: []const []const u8 = &.{}; @@ -1427,7 +1898,7 @@ pub const Chart = struct { try appendCellGeoJson(a, &out, &cell, want.items, &n); } else { const rd = ls.reader orelse continue; - var cb: CellBytes = .{}; + var cb: ChartBytes = .{}; if (!rd(ls.reader_user, lc.index, &cb)) continue; defer freeCellBytes(&cb); var ups: []const []const u8 = &.{}; @@ -1452,13 +1923,6 @@ pub const Chart = struct { return try gpa.dupe(u8, out.items); } - /// Drop the in-memory tile cache (bounds memory in long-running hosts). - pub fn clearCache(self: *Chart) void { - var it = self.cache.valueIterator(); - while (it.next()) |v| gpa.free(v.*); - self.cache.clearRetainingCapacity(); - } - /// The distinct SCAMIN denominators present in the source, ascending. The host /// publishes these as the live SCAMIN manifest so its style builds one native /// per-value bucket layer per denominator (host-canonical-backend.md §2). A baked @@ -1503,7 +1967,7 @@ pub fn renderFeature( w: u32, h: u32, palette: render.resolve.PaletteId, - settings: *const render.resolve.MarinerSettings, + settings: *const render.resolve.Settings, bg: []const u8, output: render.pixel.Output, ) ![]u8 { @@ -1529,10 +1993,10 @@ pub fn renderFeature( defer store.deinit(); // Complex-linestyle table (idempotent), same as renderView. - var ls_srcs = std.ArrayList(@import("assets").LineStyleSrc).empty; + var ls_srcs = std.ArrayList(@import("style").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); + scene.linestyle.registerLinestylesXml(gpa, ls_srcs.items); // Always show the previewed feature regardless of its SCAMIN at the framing zoom. var s = settings.*; @@ -1580,7 +2044,7 @@ pub fn renderCellView( w: u32, h: u32, palette: render.resolve.PaletteId, - settings: *const render.resolve.MarinerSettings, + settings: *const render.resolve.Settings, output: render.pixel.Output, highlight: ?Highlight, ) ![]u8 { @@ -1606,10 +2070,10 @@ pub fn renderCellView( defer store.deinit(); // Complex-linestyle table (idempotent), same as renderView / renderFeature. - var ls_srcs = std.ArrayList(@import("assets").LineStyleSrc).empty; + var ls_srcs = std.ArrayList(@import("style").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); + scene.linestyle.registerLinestylesXml(gpa, ls_srcs.items); const pt: f32 = @floatCast(256.0 * std.math.pow(f64, 2.0, zoom - @round(zoom))); var ps = render.pixel.PixelSurface.initView(a, &colors, palette, settings, zoom, w, h, pt, tile.EXTENT); @@ -1795,7 +2259,7 @@ fn collectScaminCells(ls: *LazySource, set: *std.AutoHashMap(u32, void)) void { // Streaming cell with no resident bytes: read them via the host reader for // this scan only, then free (the normal tile path reads them again on demand). const rd = ls.reader orelse continue; - var cb: CellBytes = .{}; + var cb: ChartBytes = .{}; if (!rd(ls.reader_user, lc.index, &cb)) continue; defer freeCellBytes(&cb); var ups: []const []const u8 = &.{}; @@ -1884,7 +2348,17 @@ const BakeWork = struct { portrayal_plain = cp.plain; portrayal_simplified = cp.simplified; } else |_| {} - if (c.build_geo) geo = scene.buildGeoCache(p.allocator(), &cell) catch null; + // Build the geometry cache for EVERY cell, unconditionally. + // `build_geo` (cacheGeoForBand) gated it to the finer bands, but coarse cells are + // exactly the ones that hurt without it: the geo cache both cheapens per-tile + // reprojection AND lets buildLabelCache assemble each feature's parts to populate + // the label-point cache. Skipping it left the pole-of-inaccessibility (polylabel) + // search running per tile on huge coarse cells (US1GC09M: minutes). + geo = scene.buildGeoCache(p.allocator(), &cell) catch null; + // Per-feature label-point (polylabel) cache — tile-invariant, so compute it ONCE + // per cell (only for Text/centred-symbol features) instead of re-running the search + // for every tile a feature touches; the arena outlives the call via c.arenas. + cell.label_cache = scene.buildLabelCache(p.allocator(), &cell, geo, portrayal) catch null; } // M_COVR coverage + scale for per-cell quilting (allocate into the cell's own // arena before the move, so it outlives with the backend). @@ -1899,7 +2373,7 @@ const BakeWork = struct { } }; -/// Bake an ENC_ROOT (the same cells as `openCells`) into ONE PMTiles archive, +/// Bake an ENC_ROOT (the same cells as `openCharts`) into ONE PMTiles archive, /// zoom-banded per cell by compilation scale. Returns the archive bytes (free /// with `freeBytes`), or null if nothing was covered. Streams band-by-band /// (finest → coarsest, best-band dedup + the scamin-aware band handoff), holding @@ -1908,7 +2382,7 @@ const BakeWork = struct { /// `progress` (nullable) fires during the load+portray phase (stage 0) and the /// tile-bake phase (stage 1). The caller owns the input bytes for the call. pub fn bakeArchive( - cells_in: []const CellInput, + cells_in: []const ChartInput, rules_dir: ?[]const u8, minzoom: u8, maxzoom: u8, @@ -1916,6 +2390,10 @@ pub fn bakeArchive( pick_attrs: bool, progress: Progress, user: ?*anyopaque, + // A single-cell composite bake passes that cell's coverage object (from + // `scene.coverage.encodeJson`) to embed in the archive metadata; multi-cell + // bakes pass null (no single coverage to carry). + coverage_json: ?[]const u8, ) !?[]u8 { const dir = resolveRulesDir(rules_dir); @@ -1993,6 +2471,11 @@ pub fn bakeArchive( var loaded: usize = 0; var band_ord: u8 = 0; + // Union sector-figure reach across the baked cells — published as the + // archive's "light_reach" metadata so the compositor widens its tile + // addressing by the same ring the baker did (null = no figures anywhere). + var lr_union: ?[4]f64 = null; + var lr_range_m: f64 = 0; for (bake_enc.bands_fine_to_coarse) |band| { const idxs = band_idx[@intFromEnum(band)].items; const floor: bake_enc.FloorMode = if (coarsest_pop == band) .extend_min else .defer_down; @@ -2060,6 +2543,17 @@ pub fn bakeArchive( const q = bake_enc.overscaleGateDenom(be.cscl); if (q > 0) scamin_set.put(@intCast(q), {}) catch {}; } + // Fold the sector-figure reach for the archive's "light_reach" key + // (union bbox, max ground leg) while the backends are alive. + if (be.light_bbox) |lb| { + if (lr_union) |*u| { + u[0] = @min(u[0], lb[0]); + u[1] = @min(u[1], lb[1]); + u[2] = @max(u[2], lb[2]); + u[3] = @max(u[3], lb[3]); + } else lr_union = lb; + lr_range_m = @max(lr_range_m, be.light_range_m); + } } if (has_work) { // Own cells first, then the finer band's carry (bakeBand own_len split). @@ -2129,7 +2623,10 @@ pub fn bakeArchive( while (it.next()) |k| try scamin_vals.append(gpa, k.*); std.mem.sort(u32, scamin_vals.items, {}, std.sort.asc(u32)); } - const meta = try scene.metadataJson(gpa, scamin_vals.items); + var light_reach_json: ?[]const u8 = null; + defer if (light_reach_json) |lj| gpa.free(lj); + if (lr_union) |u| light_reach_json = scene.coverage.encodeLightReachJson(gpa, .{ .bbox = u, .range_m = lr_range_m }) catch null; + const meta = try scene.metadataJson(gpa, scamin_vals.items, coverage_json, light_reach_json); defer gpa.free(meta); return try sw.finishBytes(.{ .metadata_json = meta, diff --git a/src/compose/clip.zig b/src/compose/clip.zig new file mode 100644 index 0000000..6e44056 --- /dev/null +++ b/src/compose/clip.zig @@ -0,0 +1,345 @@ +//! The compositor's clip core: clip ONE cell's decoded tile features to the ground +//! it OWNS, so the compositor can merge many cells' clipped tiles into one composed +//! tile with no double-draw at a seam. +//! +//! `face` is the cell's owned rings (from `partition.ownedFace`) projected to THIS tile's +//! pixel space, i64 — `projectFace` does that projection, mirroring EXACTLY the baker's +//! `tile.project` + `tile.clipPolygon` on the cell's features, so the face and the features +//! share one pixel space and the intersection is seam-exact. Areas are intersected with the +//! face, lines clipped to inside it, points kept iff their node is inside. Reuses the +//! geometry primitives — `boolean.compute` (.intersect), `plane.clipLineInsidePolys`, +//! `boolean.pointInEvenOdd` — so there is no new set algebra here, only the mvt↔integer +//! adapters and the per-geometry-type dispatch. + +const std = @import("std"); +const mvt = @import("tiles").mvt; +const tile = @import("tiles").tile; +const geometry = @import("geometry"); +const boolean = geometry.boolean; +const plane = geometry.plane; + +const Pt = boolean.Pt; + +fn widenRing(a: std.mem.Allocator, ring: []const mvt.Point) ![]Pt { + const out = try a.alloc(Pt, ring.len); + for (ring, 0..) |p, i| out[i] = .{ .x = p.x, .y = p.y }; + return out; +} + +fn narrowRing(a: std.mem.Allocator, ring: []const Pt) ![]mvt.Point { + const out = try a.alloc(mvt.Point, ring.len); + for (ring, 0..) |p, i| out[i] = .{ .x = @intCast(p.x), .y = @intCast(p.y) }; + return out; +} + +/// A linestring carrying class=LIGHTS is a constructed sector figure (LIGHTS is a +/// point class; its only line output is emitAugFigures legs/arcs). Public so the +/// compositor's reach-ring pass can pick figures out of a neighbouring cell's tile. +pub fn isLightFigure(feat: mvt.DecodedFeature) bool { + for (feat.properties) |p| { + if (std.mem.eql(u8, p.key, "class")) { + return switch (p.value) { + .string => |v| std.mem.eql(u8, v, "LIGHTS"), + else => false, + }; + } + } + return false; +} + +/// A `masked`-tagged linestring is the meta-bounds inspection outline of ONE chart's +/// OWN extent (the §8.6.2-masked boundary complement — see scene.emitMaskedBoundary). +/// It must trace that chart's whole coverage boundary, so it is NOT clipped to the +/// owned face: clipping it there carves the outline apart wherever a finer chart owns +/// the ground on top of it, so a coarse chart's rectangle comes out in disjoint +/// fragments (the "missing edges" a meta-bounds inspection shows). Kept whole, like a +/// LIGHTS sector figure — it draws over neighbouring ground exactly as the single-chart +/// meta-bounds render would. The standard display filters the meta class out anyway. +pub fn isMaskedBoundary(feat: mvt.DecodedFeature) bool { + for (feat.properties) |p| { + if (std.mem.eql(u8, p.key, "masked")) { + return switch (p.value) { + .int => |v| v != 0, + .uint => |v| v != 0, + .boolean => |v| v, + else => false, + }; + } + } + return false; +} + +/// Clip `feat` (tile-pixel space) to `face` (the cell's owned rings, tile-pixel space) and +/// append the surviving feature(s) to `out`, or nothing if the feature is entirely outside +/// the face. Geometry is freshly allocated in `a`; `properties` are borrowed from `feat`. +/// `face` must be a simple even-odd ring-set (as `partition.ownedFace` emits). One +/// exception to face ownership: LIGHTS sector figures are kept whole (see isLightFigure). +pub fn clipFeatureToFace(a: std.mem.Allocator, out: *std.ArrayList(mvt.Feature), feat: mvt.DecodedFeature, face: []const []const Pt) !void { + switch (feat.geom_type) { + .polygon => { + const poly = try a.alloc([]const Pt, feat.parts.len); + for (feat.parts, 0..) |ring, i| poly[i] = try widenRing(a, ring); + const clipped = try boolean.compute(a, poly, face, .intersect); + if (clipped.len == 0) return; + const parts = try a.alloc([]const mvt.Point, clipped.len); + for (clipped, 0..) |ring, i| parts[i] = try narrowRing(a, ring); + try out.append(a, .{ .geom_type = .polygon, .parts = parts, .properties = feat.properties }); + }, + .linestring => { + // A LIGHTS line is always a constructed sector figure (legs/arcs around + // the light — LIGHTS itself is a point class): a fixed-size decoration + // anchored at the light, not ground. Clipping it to the owned face + // amputates the figure at the seam, so keep it WHOLE — it draws over + // neighbouring ground exactly as a single-chart render would. + if (isLightFigure(feat) or isMaskedBoundary(feat)) { + const parts = try a.alloc([]const mvt.Point, feat.parts.len); + for (feat.parts, 0..) |part, i| parts[i] = try a.dupe(mvt.Point, part); + try out.append(a, .{ .geom_type = .linestring, .parts = parts, .properties = feat.properties }); + return; + } + var parts = std.ArrayList([]const mvt.Point).empty; + for (feat.parts) |part| { + const runs = try plane.clipLineInsidePolys(a, try widenRing(a, part), face); + for (runs) |run| { + if (run.len >= 2) try parts.append(a, try narrowRing(a, run)); + } + } + if (parts.items.len == 0) return; + try out.append(a, .{ .geom_type = .linestring, .parts = try parts.toOwnedSlice(a), .properties = feat.properties }); + }, + .point => { + // MVT points: one part per point. Keep the parts whose node is inside the face. + var parts = std.ArrayList([]const mvt.Point).empty; + for (feat.parts) |part| { + if (part.len == 0) continue; + if (boolean.pointInEvenOdd(face, part[0].x, part[0].y)) { + try parts.append(a, try a.dupe(mvt.Point, part)); + } + } + if (parts.items.len == 0) return; + try out.append(a, .{ .geom_type = .point, .parts = try parts.toOwnedSlice(a), .properties = feat.properties }); + }, + .unknown => {}, + } +} + +/// Project a cell's owned face — `partition.ownedFace` rings in integer lon/lat (degrees × +/// 10⁷) — into tile `(z, tx, ty)` pixel space and clip to the tile box, returning the even-odd +/// ring set `clipFeatureToFace` expects (boolean.Pt, i64), freshly allocated in `a`. This is +/// the SAME projection the per-cell baker applied to that tile's features (`tile.project` + +/// `tile.clipPolygon` over `Box.default(EXTENT, BUFFER)` with the same round), so the face and +/// the decoded features live in one pixel space and the clip is seam-exact. Rings collapsing +/// below a triangle are dropped; an empty result means the cell owns no pixels in this tile. +pub fn projectFace(a: std.mem.Allocator, face: []const []const Pt, z: u8, tx: u32, ty: u32) ![]const []const Pt { + var rings = std.ArrayList([]const Pt).empty; + for (face) |ring| { + const proj = try a.alloc(mvt.Point, ring.len); + for (ring, 0..) |p, i| proj[i] = tile.project( + @as(f64, @floatFromInt(p.x)) / 1e7, + @as(f64, @floatFromInt(p.y)) / 1e7, + z, + tx, + ty, + tile.EXTENT, + ); + const clipped = try tile.clipPolygon(a, proj, tile.Box.default(tile.EXTENT, tile.BUFFER)); + if (clipped.len < 3) continue; + const widened = try a.alloc(Pt, clipped.len); + for (clipped, 0..) |cp, i| widened[i] = .{ .x = cp.x, .y = cp.y }; + try rings.append(a, widened); + } + return rings.toOwnedSlice(a); +} + +// =========================================================================== +// Tests +// =========================================================================== + +const testing = std.testing; + +fn boxRing(a: std.mem.Allocator, x0: i32, y0: i32, x1: i32, y1: i32) ![]mvt.Point { + const r = try a.alloc(mvt.Point, 4); + r[0] = .{ .x = x0, .y = y0 }; + r[1] = .{ .x = x1, .y = y0 }; + r[2] = .{ .x = x1, .y = y1 }; + r[3] = .{ .x = x0, .y = y1 }; + return r; +} + +// One decoded feature over the given parts (DecodedFeature.parts is mutable [][]Point). +fn decoded(a: std.mem.Allocator, gt: mvt.GeomType, parts: []const []mvt.Point) !mvt.DecodedFeature { + const p = try a.alloc([]mvt.Point, parts.len); + for (parts, 0..) |part, i| p[i] = part; + return .{ .geom_type = gt, .parts = p, .properties = &.{} }; +} + +fn boxFace(a: std.mem.Allocator, x0: i64, y0: i64, x1: i64, y1: i64) ![]const []const Pt { + const r = try a.alloc(Pt, 4); + r[0] = .{ .x = x0, .y = y0 }; + r[1] = .{ .x = x1, .y = y0 }; + r[2] = .{ .x = x1, .y = y1 }; + r[3] = .{ .x = x0, .y = y1 }; + const rings = try a.alloc([]const Pt, 1); + rings[0] = r; + return rings; +} + +test "clipFeatureToFace: area is intersected with the face" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A whole-tile area, clipped to the face box [1000,1000]..[3000,3000]. + const ring = try boxRing(a, 0, 0, 4096, 4096); + const feat = try decoded(a, .polygon, &.{ring}); + const face = try boxFace(a, 1000, 1000, 3000, 3000); + + var out = std.ArrayList(mvt.Feature).empty; + try clipFeatureToFace(a, &out, feat, face); + try testing.expectEqual(@as(usize, 1), out.items.len); + + // Widen the output back and check: (2000,2000) inside, (500,500) outside. + const of = out.items[0]; + const wrings = try a.alloc([]const Pt, of.parts.len); + for (of.parts, 0..) |r, i| wrings[i] = try widenRing(a, r); + try testing.expect(boolean.pointInEvenOdd(wrings, 2000, 2000)); + try testing.expect(!boolean.pointInEvenOdd(wrings, 500, 500)); +} + +test "clipFeatureToFace: point kept iff inside the face" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const inside = try a.alloc(mvt.Point, 1); + inside[0] = .{ .x = 2000, .y = 2000 }; + const outside = try a.alloc(mvt.Point, 1); + outside[0] = .{ .x = 500, .y = 500 }; + const feat = try decoded(a, .point, &.{ inside, outside }); + const face = try boxFace(a, 1000, 1000, 3000, 3000); + + var out = std.ArrayList(mvt.Feature).empty; + try clipFeatureToFace(a, &out, feat, face); + try testing.expectEqual(@as(usize, 1), out.items.len); // the feature survives + try testing.expectEqual(@as(usize, 1), out.items[0].parts.len); // only the inside point + try testing.expectEqual(@as(i32, 2000), out.items[0].parts[0][0].x); +} + +test "clipFeatureToFace: line clipped to the part inside the face" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A horizontal line across the tile at y=2000, clipped to face x∈[1000,3000]. + const line = try a.alloc(mvt.Point, 2); + line[0] = .{ .x = 0, .y = 2000 }; + line[1] = .{ .x = 4096, .y = 2000 }; + const feat = try decoded(a, .linestring, &.{line}); + const face = try boxFace(a, 1000, 1000, 3000, 3000); + + var out = std.ArrayList(mvt.Feature).empty; + try clipFeatureToFace(a, &out, feat, face); + try testing.expectEqual(@as(usize, 1), out.items.len); + const kept = out.items[0].parts[0]; + try testing.expectEqual(@as(i32, 1000), @min(kept[0].x, kept[kept.len - 1].x)); + try testing.expectEqual(@as(i32, 3000), @max(kept[0].x, kept[kept.len - 1].x)); +} + +test "clipFeatureToFace: a masked meta-bounds outline is kept whole, not clipped to the face" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // The same horizontal line as above, but tagged masked=1 (a chart's own coverage + // outline): it must survive whole — the finer chart's face must NOT carve it, or + // the coarse chart's boundary comes out in fragments. + const line = try a.alloc(mvt.Point, 2); + line[0] = .{ .x = 0, .y = 2000 }; + line[1] = .{ .x = 4096, .y = 2000 }; + var feat = try decoded(a, .linestring, &.{line}); + feat.properties = &.{.{ .key = "masked", .value = .{ .int = 1 } }}; + const face = try boxFace(a, 1000, 1000, 3000, 3000); + + var out = std.ArrayList(mvt.Feature).empty; + try clipFeatureToFace(a, &out, feat, face); + try testing.expectEqual(@as(usize, 1), out.items.len); + const kept = out.items[0].parts[0]; + try testing.expectEqual(@as(usize, 2), kept.len); // untouched: both original endpoints survive + try testing.expectEqual(@as(i32, 0), kept[0].x); + try testing.expectEqual(@as(i32, 4096), kept[1].x); +} + +test "clipFeatureToFace: feature entirely outside the face is dropped" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const ring = try boxRing(a, 0, 0, 500, 500); + const feat = try decoded(a, .polygon, &.{ring}); + const face = try boxFace(a, 1000, 1000, 3000, 3000); + + var out = std.ArrayList(mvt.Feature).empty; + try clipFeatureToFace(a, &out, feat, face); + try testing.expectEqual(@as(usize, 0), out.items.len); +} + +test "projectFace: a lon/lat face box inside the tile lands vertex-for-vertex at tile.project" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Web-mercator tile z2/(1,1) spans lon [-90,0], lat [0,66.51]. A face box in integer + // lon/lat (degrees × 1e7) fully INSIDE it, so the box clip is a no-op and every projected + // vertex must equal tile.project of the same lon/lat (projectFace adds no transform beyond + // E7→deg + the shared clip). SW→SE→NE→NW ring order. + const box = [_]Pt{ + .{ .x = -600_000_000, .y = 200_000_000 }, // lon -60, lat 20 + .{ .x = -300_000_000, .y = 200_000_000 }, // lon -30, lat 20 + .{ .x = -300_000_000, .y = 500_000_000 }, // lon -30, lat 50 + .{ .x = -600_000_000, .y = 500_000_000 }, // lon -60, lat 50 + }; + const rings = [_][]const Pt{&box}; + + const z: u8 = 2; + const tx: u32 = 1; + const ty: u32 = 1; + const face_px = try projectFace(a, &rings, z, tx, ty); + try testing.expectEqual(@as(usize, 1), face_px.len); + try testing.expectEqual(@as(usize, 4), face_px[0].len); + + for (box, 0..) |p, i| { + const want = tile.project( + @as(f64, @floatFromInt(p.x)) / 1e7, + @as(f64, @floatFromInt(p.y)) / 1e7, + z, + tx, + ty, + tile.EXTENT, + ); + try testing.expectEqual(@as(i64, want.x), face_px[0][i].x); + try testing.expectEqual(@as(i64, want.y), face_px[0][i].y); + // Inside the tile → inside the pixel box. + try testing.expect(face_px[0][i].x >= 0 and face_px[0][i].x <= tile.EXTENT); + try testing.expect(face_px[0][i].y >= 0 and face_px[0][i].y <= tile.EXTENT); + } + // Axis sanity: east lon → larger x; north lat (larger lat) → smaller y (y grows down). + try testing.expect(face_px[0][1].x > face_px[0][0].x); // lon -30 east of -60 + try testing.expect(face_px[0][2].y < face_px[0][1].y); // lat 50 north of lat 20 +} + +test "projectFace: a ring fully outside the tile box is dropped" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A face far to the east (lon ≈ +160) cannot touch tile z2/(1,1) (lon [-90,0]). + const east = [_]Pt{ + .{ .x = 1_600_000_000, .y = 100_000_000 }, + .{ .x = 1_700_000_000, .y = 100_000_000 }, + .{ .x = 1_700_000_000, .y = 200_000_000 }, + .{ .x = 1_600_000_000, .y = 200_000_000 }, + }; + const rings = [_][]const Pt{&east}; + const face_px = try projectFace(a, &rings, 2, 1, 1); + try testing.expectEqual(@as(usize, 0), face_px.len); +} diff --git a/src/compose/compose.zig b/src/compose/compose.zig new file mode 100644 index 0000000..82be8d1 --- /dev/null +++ b/src/compose/compose.zig @@ -0,0 +1,597 @@ +//! compose — the runtime tile compositor. Given N per-cell PMTiles archives plus +//! an ownership partition, it serves any (z, x, y) tile ON DEMAND by clipping each +//! owning cell's tile to the face it owns and stitching the result. Separate from +//! baking: it reads already-baked archives and never parses S-57 or runs +//! portrayal, so it depends only on the tile/geometry/coverage leaves. +//! +//! ComposeSource — resident compositor over mmap'd archives + a partition +//! composeTile — compose one tile (the stateless core ComposeSource uses) +//! openComposeSourceFiles — open archives from disk, load or build the partition +//! openComposeSourceCharts — same, borrowing already-open charts' readers + coverage +//! clip — the per-face clip-to-owned-geometry core (submodule) + +const std = @import("std"); +const pmtiles = @import("tiles").pmtiles; +const mvt = @import("tiles").mvt; +const mlt = @import("tiles").mlt; +const gzip = @import("tiles").gzip; +const tile = @import("tiles").tile; +const band = @import("tiles").band; +const filemap = @import("tiles").filemap; +const geometry = @import("geometry"); +const coverage = @import("coverage"); +const s57 = @import("s57"); + +/// The per-face clip-to-owned-geometry core: project an owned face to tile space +/// and clip each feature to it. Pure over tiles + geometry. +pub const clip = @import("clip.zig"); + +pub const LoadedCov = struct { + name: []const u8, // DSNM stem + date: []const u8, // DSID issue/update date (YYYYMMDD) + cscl: i32, // compilation scale (1:N) + coverage: []const []const []const s57.LonLat, // M_COVR(CATCOV=1) rings + bounds: [4]f64, // [w,s,e,n] over the coverage + light_reach: ?coverage.LightReach = null, // sector-figure reach ("light_reach" metadata) +}; + +pub fn toPlaneCells(a: std.mem.Allocator, loaded: []const LoadedCov) ![]geometry.plane.Cell { + const n = loaded.len; + const rank = try a.alloc(usize, n); + for (rank, 0..) |*v, i| v.* = i; + std.mem.sort(usize, rank, loaded, struct { + fn lt(ls: []const LoadedCov, x: usize, y: usize) bool { + return geometry.partition.ordersBeforeKeys(ls[x].date, ls[x].name, ls[y].date, ls[y].name); + } + }.lt); + const order = try a.alloc(u64, n); + for (rank, 0..) |ci, r| order[ci] = r; + + const cells = try a.alloc(geometry.plane.Cell, n); + for (loaded, 0..) |lc, i| { + const out = try a.alloc(geometry.plane.Poly, lc.coverage.len); + for (lc.coverage, 0..) |feat, fi| { + const rings = try a.alloc([]const geometry.plane.Pt, feat.len); + for (feat, 0..) |ring, ri| { + const pts = try a.alloc(geometry.plane.Pt, ring.len); + for (ring, 0..) |p, pi| pts[pi] = .{ .x = p.lon_e7, .y = p.lat_e7 }; + rings[ri] = pts; + } + out[fi] = rings; + } + cells[i] = .{ + .cscl = lc.cscl, + .band_floor = band.bandZooms(band.bandOf(lc.cscl)).min, + .order = order[i], + .cov1 = out, + .light_bbox = if (lc.light_reach) |lr| lr.bbox else null, + .light_range_m = if (lc.light_reach) |lr| lr.range_m else 0, + }; + } + return cells; +} + +// A normalised web-mercator world axis coordinate ([0,1]) -> tile index at `scale` +// (= 2^z), clamped to [0, scale-1]. +pub fn worldAxisToTile(w: f64, scale: f64) u32 { + const f = @floor(w * scale); + if (f < 0) return 0; + return @intFromFloat(@min(f, scale - 1)); +} + +// =========================================================================== +// Per-cell composite — the on-demand tile compositor +// =========================================================================== +// +// Combine N per-cell PMTiles (each native-band-scale, its M_COVR coverage embedded in the +// metadata) into ONE merged PMTiles driven by the ownership partition. At every output tile, +// each owning cell's decoded features are clipped to the ground it OWNS (partition.ownedFace, +// projected into the tile) and concatenated per layer. The faces are a disjoint partition, so +// there is no double-draw at a seam and no z-order re-sort — S-52 draw priority rides the +// per-feature `draw_prio` property, which the style sorts client-side (so feature order within +// a tile is cosmetic). This retires the streaming in-bake cross-cell combiner: the per-cell +// bakes stay dumb + cacheable, and all cross-cell logic is precomputed as the partition. + +const N_COMPOSE_LAYERS = mvt.VECTOR_LAYERS.len; + +// The tile-index cover (nw..se) of an owner face's lon/lat bbox at zoom `scale = 1< bb.tx1 or ty < bb.ty0 or ty > bb.ty1) continue; + + var grid = try geometry.plane.EdgeGrid.init(ta, face.owned, tileWidthE7(z)); + defer grid.deinit(); + const cls = grid.classify(tileClassifyBox(z, tx, ty)); + if (cls == .full) continue; // owns none of this tile + owned = true; + if (!(try ownerHasTile(readers[ci], cscl, z, tx, ty))) continue; + if (cls == .empty) { // owns the whole tile: its face projection can't be empty + verbatim = ci; + try slots.append(ta, @intCast(slot)); + continue; + } + const face_px = try compose.projectFace(ta, face.owned, z, tx, ty); + if (face_px.len == 0) continue; + try slots.append(ta, @intCast(slot)); + } + + // Reach ring (spec §2.3, the cross-TILE half): a cell owning ground at this + // tier — just none in this tile — can still have light sector figures + // sweeping in, and the bake addressed this tile for exactly that reach + // (buildTileMap's lightReachTiles ring around the cell's light_bbox). Apply + // the SAME ring here: consult each such cell's archive and let + // composeSeamTile take only its constructed LIGHTS figures, whole. + // Without this the figures amputate exactly at the composed-tile boundary. + var reach = std.ArrayList(u32).empty; + { + var contributed = try ta.alloc(bool, part.cells.len); + @memset(contributed, false); + for (slots.items) |slot| contributed[map.faces[slot].index] = true; + var seen = try ta.alloc(bool, part.cells.len); + @memset(seen, false); + for (map.faces) |face| { + if (face.owned.len == 0) continue; + const ci = face.index; + if (contributed[ci] or seen[ci]) continue; + seen[ci] = true; + const c = part.cells[ci]; + const lb = c.light_bbox orelse continue; + const r = tile.lightReachTiles(c.light_range_m, z, (lb[1] + lb[3]) * 0.5); + const w_tl = tile.lonLatToWorld(lb[0], lb[3]); + const w_br = tile.lonLatToWorld(lb[2], lb[1]); + const fx: f64 = @floatFromInt(tx); + const fy: f64 = @floatFromInt(ty); + if (fx + 1.0 <= w_tl[0] * scale - r or fx >= w_br[0] * scale + r or + fy + 1.0 <= w_tl[1] * scale - r or fy >= w_br[1] * scale + r) continue; + if (!(try ownerHasTile(readers[ci], c.cscl, z, tx, ty))) continue; + try reach.append(ta, @intCast(ci)); + } + } + + // Verbatim fast path: only when no reach cell contributes (else the figures + // must merge, so the tile seam-composes; content-identical for the owner). + if (reach.items.len == 0) if (verbatim) |ci| { + if (want_gzip) { + if (try readers[ci].getCompressed(z, tx, ty)) |blob| return .{ .tile = try gpa.dupe(u8, blob), .owned = true }; + } else if (try readers[ci].getTile(ta, z, tx, ty)) |raw| return .{ .tile = try gpa.dupe(u8, raw), .owned = true }; + }; + if (slots.items.len == 0 and reach.items.len == 0) return .{ .tile = null, .owned = owned }; + + const enc = (try composeSeamTile(ta, part, map, readers, slots.items, reach.items, z, tx, ty)) orelse return .{ .tile = null, .owned = owned }; + // want_gzip → match the archive's stored (gzipped) bytes; else hand back the raw MLT. + const bytes = if (want_gzip) try pmtiles.StreamWriter.gzipTile(gpa, enc) else try gpa.dupe(u8, enc); + return .{ .tile = bytes, .owned = true }; +} + +/// The outcome of composing one tile: its bytes (null if nothing rendered) and whether the ownership +/// partition says a cell SHOULD render here. `tile == null and owned` = expected-but-empty (a cell +/// owns the ground but produced nothing — transient during a bake, suspect once a bake is done); +/// `tile == null and !owned` = true empty (no cell owns this ground — open ocean, safe to cache). +pub const TileResult = struct { tile: ?[]u8, owned: bool }; + +/// A resident compositor: the per-cell archives held mmap'd and the ownership partition built once +/// (or loaded from a sidecar), so `serve` composes any tile without a whole-district pass. Open once, +/// serve many, deinit. Only coverage-carrying archives are kept; cell index == reader index. This is +/// the runtime backing for on-demand serving — the batch is for producing a full archive; this is for +/// a camera asking for tiles. +pub const ComposeSource = struct { + gpa: std.mem.Allocator, + arena: std.heap.ArenaAllocator, // owns the readers/maps arrays + adapted cells (borrowed by part) + maps: []const []align(std.heap.page_size_min) const u8, + readers: []const *pmtiles.Reader, + // A files-open owns its readers + mmaps (deinit closes them); a charts-open + // borrows them from the charts, which must outlive this source. + owns_archives: bool = true, + part: geometry.partition.Partition, + minz: u8, + maxz: u8, + loop_max: u8, // deepest zoom the sources can serve (native windows + one fill-up overscale zoom) + bounds: [4]f64, // union coverage [west, south, east, north] in degrees + + /// Compose one tile → raw (decompressed) MLT + the ownership flag (gpa-owned bytes; null when + /// nothing rendered — `owned` then says whether a cell SHOULD have). This is what a live tile + /// server hands its HTTP layer, which gzips on the wire. Byte-faithful to the batch. + pub fn tile(self: *ComposeSource, gpa: std.mem.Allocator, z: u8, tx: u32, ty: u32) !TileResult { + return composeTile(gpa, &self.part, self.readers, z, tx, ty, false); + } + /// Serialize the resident ownership partition to a sidecar blob (gpa-owned) a later open can + /// load to skip the owned-face build. + pub fn serializePartition(self: *ComposeSource, gpa: std.mem.Allocator) ![]u8 { + return geometry.partition.serialize(gpa, &self.part); + } + pub fn deinit(self: *ComposeSource) void { + const gpa = self.gpa; + self.part.deinit(); + if (self.owns_archives) { + for (self.readers) |rp| rp.deinit(); + for (self.maps) |m| filemap.unmap(m); + } + self.arena.deinit(); + gpa.destroy(self); + } + + /// Open over per-chart PMTiles paths (mmap'd; the chart set is never fully + /// resident). Null when no archive carries coverage. + pub const openFiles = openSourceFiles; + /// Open over already-open charts' archives (everything is BORROWED — the + /// charts must outlive this source). Null when no archive carries coverage. + pub const open = openSourceCharts; +}; + +/// Open a resident ComposeSource over per-cell PMTiles at `paths` (mmap'd, so the cell set is never +/// fully resident). If `load_partition` is non-null and valid for this cell set the partition is +/// loaded (no build); else it is built. Returns null if no archive carries coverage. Free with +/// `ComposeSource.deinit`. +fn openSourceFiles(io: std.Io, gpa: std.mem.Allocator, paths: []const []const u8, load_partition: ?[]const u8) !?*ComposeSource { + const src = try gpa.create(ComposeSource); + src.* = .{ .gpa = gpa, .arena = std.heap.ArenaAllocator.init(gpa), .maps = &.{}, .readers = &.{}, .part = undefined, .minz = 0, .maxz = 0, .loop_max = 0, .bounds = .{ 0, 0, 0, 0 } }; + errdefer { + src.arena.deinit(); + gpa.destroy(src); + } + const a = src.arena.allocator(); + + // mmap + open each archive; keep only those carrying coverage, so readers/maps/shims stay + // aligned (cell index == reader index) for composeTile. + var readers = std.ArrayList(*pmtiles.Reader).empty; + var maps = std.ArrayList([]align(std.heap.page_size_min) const u8).empty; + var shims = std.ArrayList(LoadedCov).empty; + errdefer { + for (readers.items) |rp| rp.deinit(); + for (maps.items) |m| filemap.unmap(m); + } + for (paths) |path| { + var f = std.Io.Dir.cwd().openFile(io, path, .{}) catch continue; + const st = f.stat(io) catch { + f.close(io); + continue; + }; + const len: usize = @intCast(st.size); + if (len == 0) { + f.close(io); + continue; + } + const map = filemap.mapReadonly(f.handle, len) catch { + f.close(io); + continue; + }; + f.close(io); + const rp = a.create(pmtiles.Reader) catch { + filemap.unmap(map); + continue; + }; + rp.* = pmtiles.Reader.init(gpa, map) catch { + filemap.unmap(map); + continue; + }; + const meta = readMetaJson(a, rp) orelse { + rp.deinit(); + filemap.unmap(map); + continue; + }; + const cc = (coverage.decodeFromMetadata(a, meta) catch null) orelse { + rp.deinit(); + filemap.unmap(map); + continue; + }; + try maps.append(a, map); + try readers.append(a, rp); + try shims.append(a, .{ .name = cc.name, .date = cc.date, .cscl = cc.cscl, .coverage = cc.cov1, .bounds = covDegBounds(cc), .light_reach = cc.light_reach }); + } + if (readers.items.len == 0) { + src.arena.deinit(); + gpa.destroy(src); + return null; + } + return try finishOpen(gpa, src, readers.items, maps.items, shims.items, load_partition, true); +} + +/// One already-open per-cell archive for a compositor to compose over: the archive's +/// PMTiles reader plus the per-cell coverage embedded in its metadata. The compositor +/// BORROWS both — whatever owns them (a chart handle) must outlive the source. +pub const ChartArchive = struct { + reader: *pmtiles.Reader, + cov: coverage.ChartCoverage, +}; + +/// Open a resident ComposeSource over already-open charts' archives. Nothing is +/// opened or mmap'd here and deinit closes none of it: every archive is borrowed, +/// so the charts must OUTLIVE this source. Archives without coverage rings are +/// skipped (they can own no ground); returns null if none carries coverage. +/// `load_partition` as in `openFiles`. +fn openSourceCharts(gpa: std.mem.Allocator, archives: []const ChartArchive, load_partition: ?[]const u8) !?*ComposeSource { + const src = try gpa.create(ComposeSource); + src.* = .{ .gpa = gpa, .arena = std.heap.ArenaAllocator.init(gpa), .maps = &.{}, .readers = &.{}, .part = undefined, .minz = 0, .maxz = 0, .loop_max = 0, .bounds = .{ 0, 0, 0, 0 } }; + errdefer { + src.arena.deinit(); + gpa.destroy(src); + } + const a = src.arena.allocator(); + var readers = std.ArrayList(*pmtiles.Reader).empty; + var shims = std.ArrayList(LoadedCov).empty; + for (archives) |ar| { + if (ar.cov.cov1.len == 0) continue; + try readers.append(a, ar.reader); + try shims.append(a, .{ .name = ar.cov.name, .date = ar.cov.date, .cscl = ar.cov.cscl, .coverage = ar.cov.cov1, .bounds = covDegBounds(ar.cov), .light_reach = ar.cov.light_reach }); + } + if (readers.items.len == 0) { + src.arena.deinit(); + gpa.destroy(src); + return null; + } + return try finishOpen(gpa, src, readers.items, &.{}, shims.items, load_partition, false); +} + +// The coverage bbox (integer lon/lat e7) as degree bounds [w, s, e, n]. +fn covDegBounds(cc: coverage.ChartCoverage) [4]f64 { + return .{ + @as(f64, @floatFromInt(cc.bbox[0])) / 1e7, @as(f64, @floatFromInt(cc.bbox[1])) / 1e7, + @as(f64, @floatFromInt(cc.bbox[2])) / 1e7, @as(f64, @floatFromInt(cc.bbox[3])) / 1e7, + }; +} + +// Shared open tail over aligned (readers, shims): build (or load) the ownership +// partition, derive the zoom range + union bounds, and finish `src`. The arrays +// already live in src.arena; on error the caller's errdefer tears down whatever +// it owns per `owns_archives`. +fn finishOpen( + gpa: std.mem.Allocator, + src: *ComposeSource, + readers: []const *pmtiles.Reader, + maps: []const []align(std.heap.page_size_min) const u8, + shims: []const LoadedCov, + load_partition: ?[]const u8, + owns_archives: bool, +) !*ComposeSource { + const a = src.arena.allocator(); + var minz: u8 = 255; + var maxz: u8 = 0; + var ubox = [4]f64{ 1e9, 1e9, -1e9, -1e9 }; // union coverage [w, s, e, n] + // The floor is what the archives actually carry (a fill-up bake starts at z0; + // an archive baked before fill-up starts at its band floor), not the band model. + for (readers) |rp| minz = @min(minz, rp.header.min_zoom); + for (shims) |sh| { + const bz = band.bandZooms(band.bandOf(sh.cscl)); + minz = @min(minz, bz.min); + maxz = @max(maxz, bz.max); + ubox[0] = @min(ubox[0], sh.bounds[0]); + ubox[1] = @min(ubox[1], sh.bounds[1]); + ubox[2] = @max(ubox[2], sh.bounds[2]); + ubox[3] = @max(ubox[3], sh.bounds[3]); + } + + const cells = try toPlaneCells(a, shims); + for (cells, readers) |*c, rp| c.reach = @max(bandReach(c.cscl), rp.header.max_zoom); + + var loaded = false; + if (load_partition) |bytes| { + if (geometry.partition.deserialize(gpa, bytes, cells)) |p| { + src.part = p; + loaded = true; + } else |err| std.debug.print(" partition sidecar unusable ({s}); building\n", .{@errorName(err)}); + } + if (!loaded) src.part = try geometry.partition.build(gpa, cells); + + const fill_max = @min(maxz + band.FILLUP_DZ, band.FILLUP_CEIL); + src.maps = maps; + src.readers = readers; + src.owns_archives = owns_archives; + src.minz = minz; + src.maxz = maxz; + src.loop_max = @max(maxz, fill_max); + src.bounds = ubox; + return src; +} + +// The output-layer slot for a decoded layer name (one of mvt.VECTOR_LAYERS), or null to drop. +fn layerIndex(name: []const u8) ?usize { + for (mvt.VECTOR_LAYERS, 0..) |ln, i| { + if (std.mem.eql(u8, ln, name)) return i; + } + return null; +} + +// Decode a per-cell tile by its stored type (.mlt for our bakes, .mvt otherwise). +fn decodeTile(a: std.mem.Allocator, tt: pmtiles.TileType, raw: []const u8) ![]mvt.DecodedLayer { + return switch (tt) { + .mlt => mlt.decode(a, raw), + else => mvt.decode(a, raw), + }; +} + +// The decoded layers cell `r` contributes at (z,tx,ty): its native tile if it has one, else — +// when z is within the fill-up window just past the cell's band native max — its deepest native +// ancestor tile with the features scaled up into this descendant (overscale). null = nothing +// reachable (below native, or a coarse-only zoom beyond the fill-up window, where the client +// camera + MapLibre overzoom take over). Everything is arena-allocated in `a`. +fn ownerTile(a: std.mem.Allocator, r: *pmtiles.Reader, cscl: i32, z: u8, tx: u32, ty: u32) !?[]mvt.DecodedLayer { + const tt = r.header.tile_type; + if (try r.getTile(a, z, tx, ty)) |raw| return try decodeTile(a, tt, raw); + + const nmax = band.bandZooms(band.bandOf(cscl)).max; + if (z <= nmax or z > nmax + band.FILLUP_DZ or z > band.FILLUP_CEIL) return null; + const shift: u5 = @intCast(z - nmax); + const anc = (try r.getTile(a, nmax, tx >> shift, ty >> shift)) orelse return null; + const layers = try decodeTile(a, tt, anc); + scaleUpTile(layers, shift, tx, ty); + return layers; +} + +// The deepest zoom a cell's band ladder can serve — its native window max, or the fill-up +// overscale window just past it (`ownerTile`'s window, which FILLUP_CEIL can pull BELOW the +// native max for the finest bands — hence the @max). The band terms of `plane.Cell.reach`. +pub fn bandReach(cscl: i32) u8 { + const nmax = band.bandZooms(band.bandOf(cscl)).max; + return @max(nmax, @min(nmax + band.FILLUP_DZ, band.FILLUP_CEIL)); +} + +// Cheap existence mirror of `ownerTile` (directory probes only — no decompress, no decode): +// would it return content for cell `r` at (z,tx,ty)? Must stay in lockstep with it — the +// tile-major compositor's discovery pass uses this to reproduce the compose predicate, and +// the two passes must agree on which tiles compose. +fn ownerHasTile(r: *pmtiles.Reader, cscl: i32, z: u8, tx: u32, ty: u32) !bool { + if ((try r.getCompressed(z, tx, ty)) != null) return true; + const nmax = band.bandZooms(band.bandOf(cscl)).max; + if (z <= nmax or z > nmax + band.FILLUP_DZ or z > band.FILLUP_CEIL) return false; + const shift: u5 = @intCast(z - nmax); + return (try r.getCompressed(nmax, tx >> shift, ty >> shift)) != null; +} + +// Scale an ancestor tile's features up into descendant (tx,ty) — the sub-cell `shift` levels +// finer: pixel (px,py) → (px< raw, + .gzip => gzip.decompress(a, raw) catch return null, + else => null, + }; +} diff --git a/src/coverage/coverage.zig b/src/coverage/coverage.zig new file mode 100644 index 0000000..5130e98 --- /dev/null +++ b/src/coverage/coverage.zig @@ -0,0 +1,239 @@ +//! Per-cell coverage carried IN the per-cell PMTiles metadata JSON. The +//! per-cell-composite bake writes one PMTiles per cell whose metadata embeds that +//! cell's M_COVR coverage + compilation scale + identity, so the compositor can +//! rebuild the ownership partition (geo.partition) from the baked archives WITHOUT +//! re-parsing the .000. This round-trips exactly the per-cell facts the partition +//! adapter consumes: coverage rings, cscl, and the (date, name) tie-break keys. +//! +//! Coordinates are S-57's native integer lon/lat (degrees × 1e7), stored VERBATIM as +//! JSON integer pairs `[lon_e7, lat_e7]` — never through f64 — so adjacent cells that +//! digitised a shared border independently round to the same integers and the boolean +//! seam-dissolve stays exact. +//! +//! `band` is a convenience field derived from cscl (bandOf); a consumer re-derives it +//! for correctness rather than trusting the stored value. `cov2` (CATCOV=2 no-data) is +//! a reserved slot, empty today — the s57 side captures only CATCOV=1. + +const std = @import("std"); +const s57 = @import("s57"); + +/// One cell's coverage + identity — everything the ownership partition needs to place +/// this cell without re-parsing. Ring geometry is `[feature][ring][point]`, matching +/// `s57.Cell.mcovrCoverage`, so a decoded value plugs straight into the partition +/// adapter. Slices are owned by whoever built or decoded the value. +pub const ChartCoverage = struct { + name: []const u8, // file basename stem (verbatim — the ownership tie-break name) + date: []const u8, // post-update DSID ISDT (YYYYMMDD), or "" if absent + cscl: i32, // compilation scale 1:N (0 = unknown) + band: u8, // derived bandOf(cscl); convenience/debug only + bbox: [4]i32, // [w, s, e, n] integer lon/lat (lon_e7/lat_e7) + cov1: []const []const []const s57.LonLat, // M_COVR CATCOV=1: [feature][ring][point] + cov2: []const []const []const s57.LonLat = &.{}, // reserved (CATCOV=2 no-data) + // Sector-figure reach (the metadata's sibling "light_reach" key, null when the + // cell constructs no figures) — decodeFromMetadata fills it so the compositor + // can widen tile addressing without re-portraying the cell. + light_reach: ?LightReach = null, +}; + +/// Sector-figure reach summary carried beside "coverage" in the per-cell archive +/// metadata (key "light_reach", written only when the cell's portrayal constructs +/// light sector figures): the union bbox of the figure-bearing anchors plus the +/// max ground-length directional-leg span. The compositor widens its tile +/// addressing with the SAME ring the baker used (tiles.tile.lightReachTiles), so +/// legs/arcs survive into neighbouring tiles the cell owns no ground in. +pub const LightReach = struct { + bbox: [4]f64, // [w, s, e, n] degrees — union of figure-bearing anchors + range_m: f64 = 0, // max ground-length leg (directional lights), metres +}; + +/// Serialize a light-reach summary to the JSON object embedded under the +/// metadata's "light_reach" key. Caller owns the returned bytes. +pub fn encodeLightReachJson(a: std.mem.Allocator, lr: LightReach) ![]u8 { + return std.json.Stringify.valueAlloc(a, lr, .{}); +} + +/// Build a ChartCoverage from a parsed cell. `name` is the caller's identity string +/// (the file basename stem, matching the coverage loader / ownership oracle) and +/// `band` the caller's `bake_enc.bandOf(cscl)` tag; both keep this module free of the +/// band + loader policy. Coverage rings are allocated in `a`; `name`/`date` are +/// borrowed (encode before the cell is freed). +pub fn fromCell(a: std.mem.Allocator, cell: *const s57.Cell, name: []const u8, band: u8) ChartCoverage { + const cov1 = cell.mcovrCoverage(a); + return .{ + .name = name, + .date = cell.dsid.isdt, + .cscl = cell.params.cscl, + .band = band, + .bbox = bboxOf(cov1), + .cov1 = cov1, + }; +} + +fn bboxOf(polys: []const []const []const s57.LonLat) [4]i32 { + var b = [4]i32{ std.math.maxInt(i32), std.math.maxInt(i32), std.math.minInt(i32), std.math.minInt(i32) }; + var any = false; + for (polys) |poly| for (poly) |ring| for (ring) |p| { + any = true; + b[0] = @min(b[0], p.lon_e7); + b[1] = @min(b[1], p.lat_e7); + b[2] = @max(b[2], p.lon_e7); + b[3] = @max(b[3], p.lat_e7); + }; + return if (any) b else .{ 0, 0, 0, 0 }; +} + +// ---- JSON round-trip ----------------------------------------------------- +// +// The wire form uses `[2]i32` points so `std.json` emits compact `[lon,lat]` pairs +// (half the bytes of `{lon_e7,lat_e7}` objects) that round-trip losslessly as +// integers. The on-disk value lives under the "coverage" key of the PMTiles metadata +// object; `encodeJson` emits that value, the writer splices it in. + +const P = [2]i32; + +const Dto = struct { + v: u32 = 0, + name: []const u8 = "", + date: []const u8 = "", + cscl: i32 = 0, + band: u8 = 0, + bbox: [4]i32 = .{ 0, 0, 0, 0 }, + cov1: []const []const []const P = &.{}, + cov2: []const []const []const P = &.{}, +}; + +// The metadata object as far as this module cares: everything else (name, format, +// vector_layers, scamin) is skipped via ignore_unknown_fields. +const Envelope = struct { coverage: ?Dto = null, light_reach: ?LightReach = null }; + +/// Serialize `cov` to the JSON object embedded under the metadata's "coverage" key. +/// Caller owns the returned bytes (allocated in `a`). +pub fn encodeJson(a: std.mem.Allocator, cov: ChartCoverage) ![]u8 { + const dto = Dto{ + .v = 1, + .name = cov.name, + .date = cov.date, + .cscl = cov.cscl, + .band = cov.band, + .bbox = cov.bbox, + .cov1 = try toWire(a, cov.cov1), + .cov2 = try toWire(a, cov.cov2), + }; + return std.json.Stringify.valueAlloc(a, dto, .{}); +} + +/// Extract the coverage embedded in a PMTiles metadata JSON blob, or null if absent / +/// unparseable. The whole result (rings + strings) is allocated in `a`. +pub fn decodeFromMetadata(a: std.mem.Allocator, metadata_json: []const u8) !?ChartCoverage { + var parsed = std.json.parseFromSlice(Envelope, a, metadata_json, .{ .ignore_unknown_fields = true }) catch return null; + defer parsed.deinit(); // frees the DTO's own arena; the copies below live in `a` + const dto = parsed.value.coverage orelse return null; + return ChartCoverage{ + .name = try a.dupe(u8, dto.name), + .date = try a.dupe(u8, dto.date), + .cscl = dto.cscl, + .band = dto.band, + .bbox = dto.bbox, + .cov1 = try fromWire(a, dto.cov1), + .cov2 = try fromWire(a, dto.cov2), + .light_reach = parsed.value.light_reach, // POD — safe past parsed.deinit() + }; +} + +fn toWire(a: std.mem.Allocator, src: []const []const []const s57.LonLat) ![]const []const []const P { + const polys = try a.alloc([]const []const P, src.len); + for (src, 0..) |poly, i| { + const rings = try a.alloc([]const P, poly.len); + for (poly, 0..) |ring, j| { + const pts = try a.alloc(P, ring.len); + for (ring, 0..) |p, k| pts[k] = .{ p.lon_e7, p.lat_e7 }; + rings[j] = pts; + } + polys[i] = rings; + } + return polys; +} + +fn fromWire(a: std.mem.Allocator, src: []const []const []const P) ![]const []const []const s57.LonLat { + const polys = try a.alloc([]const []const s57.LonLat, src.len); + for (src, 0..) |poly, i| { + const rings = try a.alloc([]const s57.LonLat, poly.len); + for (poly, 0..) |ring, j| { + const pts = try a.alloc(s57.LonLat, ring.len); + for (ring, 0..) |p, k| pts[k] = .{ .lon_e7 = p[0], .lat_e7 = p[1] }; + rings[j] = pts; + } + polys[i] = rings; + } + return polys; +} + +// =========================================================================== +// Tests +// =========================================================================== + +const testing = std.testing; + +fn ll(lon_e7: i32, lat_e7: i32) s57.LonLat { + return .{ .lon_e7 = lon_e7, .lat_e7 = lat_e7 }; +} + +test "coverage round-trips through the metadata envelope, integers exact" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Two M_COVR features: a square, and one with a hole (exterior + interior ring). + const sq = try a.dupe(s57.LonLat, &.{ ll(-763_000_000, 386_000_000), ll(-762_000_000, 386_000_000), ll(-762_000_000, 387_000_000), ll(-763_000_000, 387_000_000) }); + const ext = try a.dupe(s57.LonLat, &.{ ll(0, 0), ll(100, 0), ll(100, 100), ll(0, 100) }); + const hole = try a.dupe(s57.LonLat, &.{ ll(40, 40), ll(60, 40), ll(60, 60), ll(40, 60) }); + const feat0 = try a.dupe([]const s57.LonLat, &.{sq}); + const feat1 = try a.dupe([]const s57.LonLat, &.{ ext, hole }); + const cov1 = try a.dupe([]const []const s57.LonLat, &.{ feat0, feat1 }); + + const cov = ChartCoverage{ + .name = "US5MD1MC", + .date = "20210115", + .cscl = 20_000, + .band = 1, + .bbox = .{ -763_000_000, 0, 100, 387_000_000 }, + .cov1 = cov1, + }; + + const inner = try encodeJson(a, cov); + // Embed under "coverage" alongside the other metadata keys the decoder must skip. + const meta = try std.fmt.allocPrint(a, "{{\"name\":\"chartplotter\",\"format\":\"pbf\",\"scamin\":[1000,2000],\"coverage\":{s}}}", .{inner}); + + const got = (try decodeFromMetadata(a, meta)) orelse return error.TestUnexpectedResult; + try testing.expectEqualStrings("US5MD1MC", got.name); + try testing.expectEqualStrings("20210115", got.date); + try testing.expectEqual(@as(i32, 20_000), got.cscl); + try testing.expectEqual(@as(u8, 1), got.band); + try testing.expectEqual([4]i32{ -763_000_000, 0, 100, 387_000_000 }, got.bbox); + try testing.expectEqual(@as(usize, 2), got.cov1.len); + try testing.expectEqual(@as(usize, 1), got.cov1[0].len); // square: one ring + try testing.expectEqual(@as(usize, 2), got.cov1[1].len); // holed: exterior + hole + // A vertex survives byte-exact (no f64 round-trip). + try testing.expectEqual(ll(-763_000_000, 386_000_000), got.cov1[0][0][0]); + try testing.expectEqual(ll(60, 60), got.cov1[1][1][2]); + try testing.expectEqual(@as(usize, 0), got.cov2.len); +} + +test "metadata without a coverage key decodes to null" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const meta = "{\"name\":\"chartplotter\",\"format\":\"pbf\",\"scamin\":[1000]}"; + try testing.expect((try decodeFromMetadata(a, meta)) == null); +} + +test "bboxOf spans all rings; empty coverage yields a zero bbox" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const ring = try a.dupe(s57.LonLat, &.{ ll(-5, -3), ll(7, -3), ll(7, 9), ll(-5, 9) }); + const feat = try a.dupe([]const s57.LonLat, &.{ring}); + const cov1 = try a.dupe([]const []const s57.LonLat, &.{feat}); + try testing.expectEqual([4]i32{ -5, -3, 7, 9 }, bboxOf(cov1)); + try testing.expectEqual([4]i32{ 0, 0, 0, 0 }, bboxOf(&.{})); +} diff --git a/src/errors.zig b/src/errors.zig new file mode 100644 index 0000000..2dfc93c --- /dev/null +++ b/src/errors.zig @@ -0,0 +1,53 @@ +//! The engine's error taxonomy. Every engine entry point that can fail returns one of +//! these; the C ABI maps each to a tile57_status (see capi.zig) and carries +//! describe() as the per-call tile57_error.message. Keep this set, the C +//! tile57_status enum, and capi's statusOf in sync. + +pub const Error = error{ + NotFound, // a path or cell was not found + IoFailed, // a file/directory could not be opened, read, or written + InvalidCell, // malformed S-57 ENC cell + InvalidArchive, // malformed PMTiles archive + InvalidPartition, // malformed or stale ownership partition + Unsupported, // valid but unsupported input + RenderFailed, // tile generation or rendering failed + OutOfMemory, // an allocation failed +}; + +/// A human-readable description of an engine error — the text a C caller reads +/// from tile57_error.message. Accepts any error (falls back to the error name) so +/// a caller can pass a raw `anyerror` from a catch. +pub fn describe(e: anyerror) []const u8 { + return switch (e) { + error.NotFound => "not found", + error.IoFailed => "I/O error", + error.InvalidCell => "malformed S-57 cell", + error.InvalidArchive => "malformed PMTiles archive", + error.InvalidPartition => "malformed or stale partition", + error.Unsupported => "unsupported input", + error.RenderFailed => "render failed", + error.OutOfMemory => "out of memory", + // Specific S-57 / ISO 8211 parse failures, propagated as the reason a cell + // was invalid so a C caller reads the exact cause. + error.ShortLeader => "ISO 8211 record shorter than its leader", + error.BadLeader => "malformed ISO 8211 leader", + error.BadAsciiInt => "malformed ASCII integer field", + error.BadAsciiDigit => "malformed ASCII digit", + error.MissingFieldTerminator => "missing ISO 8211 field terminator", + error.FieldOutOfBounds => "ISO 8211 field runs past the record", + error.ModifyMissingSpatial => "update references a missing spatial record", + error.ModifyMissingFeature => "update references a missing feature record", + error.UnknownRUIN => "unknown record update instruction (RUIN)", + error.BadFeatureRecord => "malformed feature record", + error.TileGen => "tile generation failed", + else => @errorName(e), + }; +} + +test "describe covers the taxonomy" { + const std = @import("std"); + try std.testing.expectEqualStrings("malformed S-57 cell", describe(error.InvalidCell)); + try std.testing.expectEqualStrings("I/O error", describe(error.IoFailed)); + // unknown errors fall back to the name + try std.testing.expectEqualStrings("SomethingElse", describe(error.SomethingElse)); +} diff --git a/src/geo/geo.zig b/src/geo/geo.zig deleted file mode 100644 index a96c0ce..0000000 --- a/src/geo/geo.zig +++ /dev/null @@ -1,19 +0,0 @@ -//! Integer computational geometry for the cross-band composition redesign -//! (specs/cross-band-composition-redesign.md). Phase 0 — the geometry core, not -//! yet wired to the baker, the live oracle, or the client: -//! -//! * `boolean` — a Martinez–Rueda–Feito polygon boolean (union / intersection / -//! difference / symmetric-difference) on the integer lattice, with -//! overlap-edge typing and a deterministic total order, plus `unionAll`. -//! * `plane` — the per-tier coverage partition (`ownedAtTier`), the FULL / -//! EMPTY / SEAM tile classifier (`EdgeGrid`), and `clipLineOutsidePolys`. -//! -//! Both are pure (std-only) so they self-test via `zig build test`. - -pub const boolean = @import("boolean.zig"); -pub const plane = @import("plane.zig"); - -test { - _ = boolean; - _ = plane; -} diff --git a/src/geo/boolean.zig b/src/geometry/boolean.zig similarity index 56% rename from src/geo/boolean.zig rename to src/geometry/boolean.zig index 453d84c..680b1fc 100644 --- a/src/geo/boolean.zig +++ b/src/geometry/boolean.zig @@ -1,26 +1,25 @@ //! Integer polygon boolean operations — a Martinez–Rueda–Feito sweep-line. //! -//! This is the geometry core of the cross-band composition redesign -//! (specs/cross-band-composition-redesign.md, Phase 0). It computes the -//! union / intersection / difference / symmetric-difference of two polygons -//! whose vertices live on an integer lattice (the E7 coverage lattice for the -//! Stage-0 partition, or world-pixel space for per-tile emission). Integers are -//! deliberate: adjacent ENC cells that digitise a shared seam independently -//! collapse to *exact* integer equality once snapped to the lattice, so the -//! dominant seam case is a run of coincident edges rather than a cloud of +//! This is the geometry core of the cross-band chart composition. It computes the +//! union / intersection / difference / symmetric-difference of two polygons whose +//! vertices are integers (coverage in degrees × 10⁷ for the partition, or +//! world-pixel space for per-tile emission). Integers are deliberate: adjacent ENC +//! cells that digitise a shared seam independently round to the *same* integers, so +//! the dominant seam case is a run of coincident edges rather than a cloud of //! near-misses. The sweep types those coincident (overlapping) edges //! (SAME_TRANSITION / DIFFERENT_TRANSITION / NON_CONTRIBUTING) so a shared seam //! contributes to the result exactly once. //! //! Coordinate discipline: -//! * `Pt` holds i64. E7 degrees are ±1.8e9 (fit i32), but *differences* reach -//! 3.6e9 (need i64) and orientation cross-products reach ~1.3e19 (overflow -//! i64) — every orientation / area predicate therefore promotes to i128. +//! * `Pt` holds i64. Coverage coordinates (degrees × 10⁷) are ±1.8e9 (fit i32), +//! but *differences* reach 3.6e9 (need i64) and orientation cross-products +//! reach ~1.3e19 (overflow i64) — every orientation / area predicate therefore +//! promotes to i128. //! * A proper crossing point is rational; it is computed from exact i128 -//! numerators in f64 and rounded to the nearest lattice point. Both sides of +//! numerators in f64 and rounded to the nearest integer point. Both sides of //! a seam compute the same crossing from the same integer endpoints, so the -//! snap is deterministic (bake == live) and the ≤0.5-unit error is ~0.5 cm -//! at E7. Collinear-overlap endpoints are real input vertices, so they are +//! snap is deterministic (bake == live) and the ≤0.5-unit error is ~0.5 cm. +//! Collinear-overlap endpoints are real input vertices, so they are //! reproduced exactly. //! //! Determinism: the event order and the sweep-status order share one strict @@ -29,14 +28,14 @@ //! //! Winding: the result contours are emitted as *open* rings with unspecified //! orientation — even-odd fill of the returned ring-set is the region. Callers -//! that need MVT winding hand the rings to scene.orientAreaRings (the sole +//! that need MVT winding hand the rings to mvt.orientAreaRings (the sole //! winding authority); nothing here relies on input orientation either, so //! every operand is interpreted even-odd. const std = @import("std"); const Allocator = std.mem.Allocator; -/// A lattice point. i64 holds any E7 or world-pixel coordinate with headroom. +/// A point. i64 holds any coordinate (degrees × 10⁷, or world pixels) with headroom. pub const Pt = struct { x: i64, y: i64, @@ -51,6 +50,25 @@ pub const Polygon = []const []const Pt; pub const Op = enum { intersect, unite, diff, sym_diff }; +/// Diagnostic: how many result-ring walks failed to close on a real edge since +/// process start, AFTER the robust retry — an increase across a compute() call +/// means that call's result still contains an open chain whose implicit +/// closing chord slices across the region (the visible "wedge" artifact). On a +/// parity-correct survivor graph a dead end can never happen, so any increase +/// marks a boolean bug at the exact tile being composed. Callers snapshot +/// before/after (single-threaded per compute). +pub var open_chain_walks: u64 = 0; +/// Diagnostic: how many compute() calls fell back to the region-sampled +/// in_result recomputation because the sweep-flag pass dead-ended a walk. +pub var robust_retries: u64 = 0; +/// Diagnostic: how many micro-stitch edges closed a snap-divergence gap (two +/// crossings of near-parallel edges snapping to adjacent lattice points leave +/// the survivor graph with odd-degree vertex pairs a few units apart). +pub var stitched_gaps: u64 = 0; +/// Diagnostic: open chains whose implicit closing chord exceeds 32 units — the +/// visible-artifact subset of open_chain_walks (micro chords are sub-pixel). +pub var large_open_chains: u64 = 0; + // --------------------------------------------------------------------------- // Exact predicates (i128). // --------------------------------------------------------------------------- @@ -87,6 +105,10 @@ const SweepEvent = struct { other_in_out: bool = false, edge_type: EdgeType = .normal, in_result: bool = false, + /// The robust retry could not decide this edge by region sampling (its + /// midpoint lies ON the other operand's boundary — an undetected collinear + /// overlap); the parity repair may toggle it alongside the typed seams. + undecided: bool = false, inline fn vertical(self: *const SweepEvent) bool { return self.p.x == self.other.p.x; @@ -135,7 +157,16 @@ fn segBelow(e1: *SweepEvent, e2: *SweepEvent) bool { if (a1 != 0 or a2 != 0) { // Not collinear. if (e1.p.eql(e2.p)) return e1.below(e2.other.p); // share left endpoint - if (queueBefore(e2, e1)) return e2.above(e1.p); // e2 inserted first + // A T-touch: one edge's left endpoint lies exactly ON the other segment + // (a subject vertex on a coincident seam edge is the common case). The + // on-segment point says nothing about order — decide by where the + // touching edge HEADS, else the toucher sorts below the touched edge + // and inherits its sweep flags from the wrong neighbour. + if (a1 == 0) return e1.below(e2.other.p); // e2 starts on e1 + if (queueBefore(e2, e1)) { + if (signedArea(e2.p, e2.other.p, e1.p) == 0) return e2.above(e1.other.p); // e1 starts on e2 + return e2.above(e1.p); // e2 inserted first + } return e1.below(e2.p); } // Collinear: subject below clip, then stable id — consistent with queueBefore. @@ -152,14 +183,14 @@ fn segBelow(e1: *SweepEvent, e2: *SweepEvent) bool { /// `p0`), or 2 (a collinear overlap spanning [`p0`,`p1`]). pub const Inter = struct { n: u2, p0: Pt, p1: Pt }; -/// Intersect segment a0→a1 with b0→b1 on the integer lattice. Public so the +/// Intersect segment a0→a1 with b0→b1 on integer coordinates. Public so the /// coverage clip (plane.zig) can reuse the exact classification + snapped point. pub fn segIntersect(a0: Pt, a1: Pt, b0: Pt, b1: Pt) Inter { return findIntersection(a0, a1, b0, b1); } inline fn roundDiv(a: Pt, num: i128, den: i128, d: Pt) Pt { - // a + (num/den)*d, rounded to nearest lattice point. den != 0. + // a + (num/den)*d, rounded to the nearest integer point. den != 0. const t = @as(f64, @floatFromInt(num)) / @as(f64, @floatFromInt(den)); const rx = @round(@as(f64, @floatFromInt(a.x)) + t * @as(f64, @floatFromInt(d.x))); const ry = @round(@as(f64, @floatFromInt(a.y)) + t * @as(f64, @floatFromInt(d.y))); @@ -306,7 +337,6 @@ const Sweeper = struct { const it = findIntersection(le1.p, le1.other.p, le2.p, le2.other.p); if (it.n == 0) return 0; if (it.n == 1 and (le1.p.eql(le2.p) or le1.other.p.eql(le2.other.p))) return 0; // shared endpoint only - if (it.n == 2 and le1.subject == le2.subject) return 0; // overlap within one operand: even-odd cancels, ignore if (it.n == 1) { if (!le1.p.eql(it.p0) and !le1.other.p.eql(it.p0)) try self.divideSegment(le1, it.p0, gpa); @@ -348,12 +378,19 @@ const Sweeper = struct { } if (n == 2 or (n == 3 and ev[2] != null)) { - // Segments are equal, or share their left endpoint: one carries the - // transition, the other contributes nothing. - le1.edge_type = .non_contributing; - le2.edge_type = if (le1.in_out == le2.in_out) .same_transition else .different_transition; + // Segments are equal, or share their left endpoint. Across the two + // operands one carries the transition and the other contributes + // nothing. Within ONE operand there is no transition to type: both + // stay normal, and because the overlap was subdivided into exactly + // coincident pieces, the doubled pieces cancel in the result's mod-2 + // edge reduction (even-odd: a doubled edge is no boundary). if (n == 3) try self.divideSegment(ev[2].?.other, ev[1].?.p, gpa); - return 2; // typed → recompute + if (le1.subject != le2.subject) { + le1.edge_type = .non_contributing; + le2.edge_type = if (le1.in_out == le2.in_out) .same_transition else .different_transition; + return 2; // typed → recompute + } + return 3; } if (n == 3) { // Share the right endpoint: split the longer-left segment; the now @@ -394,13 +431,14 @@ const Sweeper = struct { /// Compute `subject op clip`. Result is a freshly allocated ring-set (open /// rings, even-odd fill); free with `freePolygon`. /// -/// Precondition: each operand is a *simple* polygon — its own rings do not -/// overlap in area (holes are allowed as properly-nested reverse rings). Two -/// coincident edges are fine when they come from *different* operands (the seam -/// case, typed once), but three coincident edges — which require one operand to -/// overlap itself — are not resolved. Build operands from overlapping coverage -/// rings with `unionAll`, whose pairwise fold guarantees this. Coincident and -/// shared edges across the two operands are the designed-for common case. +/// Each operand is an even-odd ring-set. Rings of one operand may cross, touch, +/// or share collinear runs with each other (a baked MVT polygon's rings all run +/// along the same tile clip edge — the case that broke the pre-subdivision +/// design): same-operand overlaps are subdivided into exactly coincident pieces +/// which cancel mod 2 in the result reduction, same-operand crossings are +/// subdivided like any crossing. Coincident edges across the two operands are +/// the designed-for seam case, typed once. `unionAll` remains the way to fold +/// many mutually-overlapping coverages into one region whose rings are clean. pub fn compute(gpa: Allocator, subject: Polygon, clip: Polygon, op: Op) ![][]Pt { var arena_state = std.heap.ArenaAllocator.init(gpa); defer arena_state.deinit(); @@ -417,8 +455,15 @@ pub fn compute(gpa: Allocator, subject: Polygon, clip: Polygon, op: Op) ![][]Pt defer sw.status.deinit(gpa); defer sw.processed.deinit(gpa); - for (subject) |ring| try addRing(&sw, ring, true, gpa); - for (clip) |ring| try addRing(&sw, ring, false, gpa); + // Each operand's edge multiset is reduced modulo 2 BEFORE the sweep: rings + // of one operand may share collinear runs (a baked MVT polygon's rings all + // running along one tile clip edge), and a doubled coincident edge flips + // even-odd membership twice — no boundary. Cancelling those here preserves + // the operand's region exactly and guarantees the sweep never sees more + // than one subject plus one clip edge coincident, which is the pair case + // its overlap typing resolves. + try addOperand(&sw, subject, true, gpa); + try addOperand(&sw, clip, false, gpa); while (sw.queue.pop()) |se| { try sw.processed.append(gpa, se); @@ -452,15 +497,66 @@ pub fn compute(gpa: Allocator, subject: Polygon, clip: Polygon, op: Op) ![][]Pt } } - return connectEdges(gpa, sw.processed.items); + // First attempt: the sweep's transition-typed in_result flags. On a correct + // flag set every result-ring walk closes; a dead-ended walk (open chain) + // means some flag was corrupted by a degenerate interleaving the pairwise + // machinery mishandled (snapped crossing landing on a vertex, vertical at + // a seam, ...). The fallback recomputes every NORMAL edge's membership by + // exact region sampling against the ORIGINAL operands — the sweep has + // already subdivided all crossings and overlaps, so an edge's interior + // lies strictly on one side of the other operand's boundary and the + // midpoint test is unambiguous (coincident seam edges keep their typing, + // which the sampling cannot decide and the sweep handles well). Both + // passes are pure functions of the input geometry, so determinism holds. + const chains_before = open_chain_walks; + const large_before = large_open_chains; + const first = try connectEdges(gpa, sw.processed.items, false); + if (open_chain_walks == chains_before) return first; + freePolygon(gpa, first); + robust_retries += 1; + open_chain_walks = chains_before; // the retry's own walk re-judges health + large_open_chains = large_before; + for (sw.processed.items) |e| { + if (!(e.left and e.edge_type == .normal)) continue; + const other: Polygon = if (e.subject) clip else subject; + // Midpoint at doubled scale (exact); on-boundary keeps the sweep's call. + const mx = e.p.x + e.other.p.x; + const my = e.p.y + e.other.p.y; + if (pointOnEdgeScaled2(other, mx, my)) { + e.undecided = true; + continue; + } + const inside_other = pointInEvenOddScaled2(other, mx, my); + e.in_result = switch (op) { + .intersect => inside_other, + .unite => !inside_other, + .diff => if (e.subject) !inside_other else inside_other, + .sym_diff => true, + }; + } + return connectEdges(gpa, sw.processed.items, true); } -fn addRing(sw: *Sweeper, ring: []const Pt, subject: bool, gpa: Allocator) !void { - if (ring.len < 3) return; - var j = ring.len - 1; - for (ring, 0..) |p, i| { - try sw.addEdge(ring[j], p, subject, gpa); - j = i; +/// Add one operand's rings to the sweep with its self-overlapping collinear +/// runs cancelled (reduceEdges). Survivors are added in reduceEdges' sorted +/// order so event ids — the deterministic tie-break — are a function of +/// geometry alone. +fn addOperand(sw: *Sweeper, rings: Polygon, subject: bool, gpa: Allocator) !void { + var scratch = std.heap.ArenaAllocator.init(gpa); + defer scratch.deinit(); + const sa = scratch.allocator(); + + var raw = std.ArrayList(HEdge).empty; + for (rings) |ring| { + if (ring.len < 3) continue; + var j = ring.len - 1; + for (ring, 0..) |p, i| { + if (!ring[j].eql(p)) try raw.append(sa, .{ .a = ring[j], .b = p }); + j = i; + } + } + for (try reduceEdges(sa, raw.items)) |e| { + try sw.addEdge(e.a, e.b, subject, gpa); } } @@ -477,7 +573,7 @@ fn addRing(sw: *Sweeper, ring: []const Pt, subject: bool, gpa: Allocator) !void // the even-degree survivor graph every non-start vertex always has an exit, so the // walk always returns to its start on a real edge; the loop's implied edges are // exactly the walked edges, keeping even-odd exact. Winding/nesting is deferred to -// scene.orientAreaRings. +// mvt.orientAreaRings. const EdgeKey = struct { ax: i64, ay: i64, bx: i64, by: i64 }; @@ -489,42 +585,178 @@ fn canonEdge(a: Pt, b: Pt) EdgeKey { return .{ .ax = b.x, .ay = b.y, .bx = a.x, .by = a.y }; } +/// The line through an edge, canonicalised so every collinear edge shares one +/// key: direction reduced by gcd with a fixed sign, plus the line's offset. +const LineKey = struct { dx: i64, dy: i64, c: i128 }; + +fn lineKeyOf(a: Pt, b: Pt) LineKey { + var dx: i64 = b.x - a.x; + var dy: i64 = b.y - a.y; + const g: i64 = @intCast(std.math.gcd(@abs(dx), @abs(dy))); + dx = @divExact(dx, g); + dy = @divExact(dy, g); + if (dx < 0 or (dx == 0 and dy < 0)) { + dx = -dx; + dy = -dy; + } + // c = d × a is constant along the line for the canonical d. + const c: i128 = @as(i128, dx) * a.y - @as(i128, dy) * a.x; + return .{ .dx = dx, .dy = dy, .c = c }; +} + +/// Split every edge of a collinear group at every group endpoint interior to +/// it, so partially-overlapping collinear edges become exactly coincident +/// pieces the mod-2 reduction can cancel. The sweep subdivides edges pairwise +/// as it discovers them, but a bundle of 3+ coincident edges (two rings of one +/// operand sharing a tile-edge run with the other operand) can finish the sweep +/// subdivided inconsistently; this pass makes the reduction see one common +/// subdivision. All split points are existing endpoints — exact, no rounding. +fn splitCollinearGroup(sa: Allocator, group: []const HEdge, out: *std.ArrayList(HEdge)) !void { + // Parametrise along the canonical direction: t = d · p (monotonic, exact, + // and on one line a parameter identifies its point uniquely). + const Cut = struct { t: i128, p: Pt }; + var cuts = std.ArrayList(Cut).empty; + const k = lineKeyOf(group[0].a, group[0].b); + for (group) |e| { + for ([_]Pt{ e.a, e.b }) |p| { + try cuts.append(sa, .{ .t = @as(i128, k.dx) * p.x + @as(i128, k.dy) * p.y, .p = p }); + } + } + std.mem.sort(Cut, cuts.items, {}, struct { + fn lt(_: void, x: Cut, y: Cut) bool { + return x.t < y.t; + } + }.lt); + for (group) |e| { + var ta = @as(i128, k.dx) * e.a.x + @as(i128, k.dy) * e.a.y; + var tb = @as(i128, k.dx) * e.b.x + @as(i128, k.dy) * e.b.y; + var pa = e.a; + var pb = e.b; + if (ta > tb) { + std.mem.swap(i128, &ta, &tb); + std.mem.swap(Pt, &pa, &pb); + } + var prev = pa; + var prev_t = ta; + for (cuts.items) |cut| { + if (cut.t <= prev_t) continue; + if (cut.t >= tb) break; + try out.append(sa, .{ .a = prev, .b = cut.p }); + prev = cut.p; + prev_t = cut.t; + } + try out.append(sa, .{ .a = prev, .b = pb }); + } +} + +/// Reduce an edge multiset to its even-odd boundary: group collinear edges, +/// split each group to one common subdivision (splitCollinearGroup), and cancel +/// exactly coincident pieces modulo 2 — a doubled edge flips membership twice +/// and is no boundary. Output is deterministically sorted (a function of the +/// input SET alone). The non-overlapping common case stays cheap: one key sort, +/// no splitting. Result is allocated in `sa`. +fn reduceEdges(sa: Allocator, raw: []const HEdge) ![]HEdge { + const keys = try sa.alloc(LineKey, raw.len); + for (raw, 0..) |e, i| keys[i] = lineKeyOf(e.a, e.b); + const idx = try sa.alloc(usize, raw.len); + for (idx, 0..) |*v, i| v.* = i; + const Ctx = struct { keys: []const LineKey, raw: []const HEdge }; + std.mem.sort(usize, idx, Ctx{ .keys = keys, .raw = raw }, struct { + fn lt(ctx: Ctx, x: usize, y: usize) bool { + const a = ctx.keys[x]; + const b = ctx.keys[y]; + if (a.dx != b.dx) return a.dx < b.dx; + if (a.dy != b.dy) return a.dy < b.dy; + if (a.c != b.c) return a.c < b.c; + const ea = canonEdge(ctx.raw[x].a, ctx.raw[x].b); + const eb = canonEdge(ctx.raw[y].a, ctx.raw[y].b); + if (ea.ax != eb.ax) return ea.ax < eb.ax; + if (ea.ay != eb.ay) return ea.ay < eb.ay; + if (ea.bx != eb.bx) return ea.bx < eb.bx; + return ea.by < eb.by; + } + }.lt); + + var out = std.ArrayList(HEdge).empty; + var grp = std.ArrayList(HEdge).empty; + var pieces = std.ArrayList(HEdge).empty; + var i: usize = 0; + while (i < idx.len) { + var j = i + 1; + while (j < idx.len and std.meta.eql(keys[idx[i]], keys[idx[j]])) j += 1; + if (j - i == 1) { + try out.append(sa, raw[idx[i]]); + i = j; + continue; + } + grp.clearRetainingCapacity(); + for (idx[i..j]) |ri| try grp.append(sa, raw[ri]); + pieces.clearRetainingCapacity(); + try splitCollinearGroup(sa, grp.items, &pieces); + // Cancel coincident pieces mod 2: sort canonically, keep odd-count runs. + std.mem.sort(HEdge, pieces.items, {}, struct { + fn lt(_: void, x: HEdge, y: HEdge) bool { + const ea = canonEdge(x.a, x.b); + const eb = canonEdge(y.a, y.b); + if (ea.ax != eb.ax) return ea.ax < eb.ax; + if (ea.ay != eb.ay) return ea.ay < eb.ay; + if (ea.bx != eb.bx) return ea.bx < eb.bx; + return ea.by < eb.by; + } + }.lt); + var p: usize = 0; + while (p < pieces.items.len) { + var q = p + 1; + const kp = canonEdge(pieces.items[p].a, pieces.items[p].b); + while (q < pieces.items.len and std.meta.eql(kp, canonEdge(pieces.items[q].a, pieces.items[q].b))) q += 1; + if ((q - p) % 2 == 1) try out.append(sa, pieces.items[p]); + p = q; + } + i = j; + } + return out.items; +} + const HEdge = struct { a: Pt, b: Pt, used: bool = false }; -fn connectEdges(gpa: Allocator, all: []const *SweepEvent) ![][]Pt { +/// Is `v` within ~2 units of segment a-b (perpendicular distance ≤ 2 with the +/// projection inside the segment, or within ~2.8 of an endpoint)? Exact i128. +fn nearSegment(v: Pt, a: Pt, b: Pt) bool { + const da: i128 = (@as(i128, v.x) - a.x) * (@as(i128, v.x) - a.x) + (@as(i128, v.y) - a.y) * (@as(i128, v.y) - a.y); + if (da <= 8) return true; + const db: i128 = (@as(i128, v.x) - b.x) * (@as(i128, v.x) - b.x) + (@as(i128, v.y) - b.y) * (@as(i128, v.y) - b.y); + if (db <= 8) return true; + const abx: i128 = @as(i128, b.x) - a.x; + const aby: i128 = @as(i128, b.y) - a.y; + const len2 = abx * abx + aby * aby; + if (len2 == 0) return false; + const avx: i128 = @as(i128, v.x) - a.x; + const avy: i128 = @as(i128, v.y) - a.y; + const cross = abx * avy - aby * avx; + if (cross * cross > 4 * len2) return false; + const dot = abx * avx + aby * avy; + return dot >= 0 and dot <= len2; +} + +fn connectEdges(gpa: Allocator, all: []const *SweepEvent, stitch: bool) ![][]Pt { var scratch = std.heap.ArenaAllocator.init(gpa); defer scratch.deinit(); const sa = scratch.allocator(); - // (1) Reduce result edges modulo 2. - var parity = std.AutoHashMap(EdgeKey, void).init(sa); + // (1) Collect the result edges and reduce them to the even-odd boundary: + // reduceEdges splits collinear bundles to one common subdivision and cancels + // coincident pieces modulo 2 (the doubled edges an even-odd operand + // contributes at a shared seam). Its deterministically sorted output also + // fixes the trace order (ring order / start vertex) as a function of + // geometry alone — the byte-stability the bake==live contract needs. + var raw = std.ArrayList(HEdge).empty; for (all) |e| { if (!(e.left and e.in_result)) continue; if (e.p.eql(e.other.p)) continue; - const key = canonEdge(e.p, e.other.p); - if (parity.contains(key)) { - _ = parity.remove(key); - } else { - try parity.put(key, {}); - } + try raw.append(sa, .{ .a = e.p, .b = e.other.p }); } - var edges = std.ArrayList(HEdge).empty; - var it = parity.keyIterator(); - while (it.next()) |k| { - try edges.append(sa, .{ .a = .{ .x = k.ax, .y = k.ay }, .b = .{ .x = k.bx, .y = k.by } }); - } - // Sort so the trace order (and hence ring order / start vertex) is a function - // of geometry alone, not of hash-map iteration — the byte-stability the - // bake==live contract needs. - std.mem.sort(HEdge, edges.items, {}, struct { - fn lt(_: void, x: HEdge, y: HEdge) bool { - if (x.a.x != y.a.x) return x.a.x < y.a.x; - if (x.a.y != y.a.y) return x.a.y < y.a.y; - if (x.b.x != y.b.x) return x.b.x < y.b.x; - return x.b.y < y.b.y; - } - }.lt); + try edges.appendSlice(sa, try reduceEdges(sa, raw.items)); var adj = std.AutoHashMap(Pt, std.ArrayList(usize)).init(sa); for (edges.items, 0..) |e, idx| { @@ -535,6 +767,169 @@ fn connectEdges(gpa: Allocator, all: []const *SweepEvent) ![][]Pt { } } + // (2a) Seam parity repair — FINAL pass only. The robust retry decides every + // NORMAL edge exactly by region sampling, but coincident cross-operand seam + // edges keep the sweep's transition typing, which snap debris on a diagonal + // seam can corrupt. Parity pins them down: on a correct boundary every + // vertex has even degree, so a seam edge whose BOTH endpoints are odd is + // included/excluded wrongly — toggling it flips both endpoint parities + // (monotone: odd count strictly drops by two per toggle, so this + // terminates). Seam edges not incident to odd vertices keep their typing. + if (stitch) { + var present = std.AutoHashMap(EdgeKey, usize).init(sa); + for (edges.items, 0..) |e, idx| try present.put(canonEdge(e.a, e.b), idx); + var deg = std.AutoHashMap(Pt, u32).init(sa); + for (edges.items) |e| { + for ([_]Pt{ e.a, e.b }) |pp| { + const gop = try deg.getOrPut(pp); + if (!gop.found_existing) gop.value_ptr.* = 0; + gop.value_ptr.* += 1; + } + } + var seams = std.ArrayList(HEdge).empty; + { + var seen = std.AutoHashMap(EdgeKey, void).init(sa); + for (all) |e| { + if (!e.left) continue; + if (e.edge_type == .normal and !e.undecided) continue; + if (e.p.eql(e.other.p)) continue; + const k = canonEdge(e.p, e.other.p); + if (seen.contains(k)) continue; + try seen.put(k, {}); + try seams.append(sa, .{ .a = e.p, .b = e.other.p }); + } + } + std.mem.sort(HEdge, seams.items, {}, struct { + fn lt(_: void, x: HEdge, y: HEdge) bool { + const ka = canonEdge(x.a, x.b); + const kb = canonEdge(y.a, y.b); + if (ka.ax != kb.ax) return ka.ax < kb.ax; + if (ka.ay != kb.ay) return ka.ay < kb.ay; + if (ka.bx != kb.bx) return ka.bx < kb.bx; + return ka.by < kb.by; + } + }.lt); + var changed = true; + while (changed) { + changed = false; + for (seams.items) |se| { + const da = deg.get(se.a) orelse 0; + const db = deg.get(se.b) orelse 0; + if (da % 2 == 0 or db % 2 == 0) continue; + const k = canonEdge(se.a, se.b); + if (present.get(k)) |idx| { + // Included wrongly: remove (swap-remove keeps indices dense). + _ = present.remove(k); + const last = edges.items.len - 1; + if (idx != last) { + edges.items[idx] = edges.items[last]; + try present.put(canonEdge(edges.items[idx].a, edges.items[idx].b), idx); + } + edges.items.len = last; + deg.getPtr(se.a).?.* -= 1; + deg.getPtr(se.b).?.* -= 1; + } else { + try edges.append(sa, .{ .a = se.a, .b = se.b }); + try present.put(k, edges.items.len - 1); + for ([_]Pt{ se.a, se.b }) |pp| { + const gop = try deg.getOrPut(pp); + if (!gop.found_existing) gop.value_ptr.* = 0; + gop.value_ptr.* += 1; + } + } + changed = true; + } + } + // Rebuild adjacency over the repaired edge set (sorted for determinism). + std.mem.sort(HEdge, edges.items, {}, struct { + fn lt(_: void, x: HEdge, y: HEdge) bool { + if (x.a.x != y.a.x) return x.a.x < y.a.x; + if (x.a.y != y.a.y) return x.a.y < y.a.y; + if (x.b.x != y.b.x) return x.b.x < y.b.x; + return x.b.y < y.b.y; + } + }.lt); + adj.clearRetainingCapacity(); + for (edges.items, 0..) |e, idx| { + for ([_]Pt{ e.a, e.b }) |pp| { + const gop = try adj.getOrPut(pp); + if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(usize).empty; + try gop.value_ptr.append(sa, idx); + } + } + } + + // (2) Micro-stitch snap-divergence gaps — FINAL pass only (the flag-driven + // first pass must dead-end loudly so the robust retry can recompute its + // memberships; stitching there would paper over flag corruption). Crossings + // of near-parallel edges can snap to ADJACENT lattice points, leaving stub + // edges whose endpoints miss their partners by a few units — odd-degree + // vertices in close pairs. Pairing each with its nearest odd peer within + // the snap radius and adding the connecting micro-edge closes the walk with + // bounded (sub-pixel) error; anything farther apart is a real defect and + // stays a countable dead end. + if (stitch) { + var odd = std.ArrayList(Pt).empty; + var deg_it = adj.iterator(); + while (deg_it.next()) |entry| { + if (entry.value_ptr.items.len % 2 != 0) try odd.append(sa, entry.key_ptr.*); + } + std.mem.sort(Pt, odd.items, {}, struct { + fn lt(_: void, x: Pt, y: Pt) bool { + if (x.x != y.x) return x.x < y.x; + return x.y < y.y; + } + }.lt); + const micro_r2: i128 = 16 * 16; + const cert_r2: i128 = 1024 * 1024; + var paired = try sa.alloc(bool, odd.items.len); + @memset(paired, false); + for (odd.items, 0..) |v, i| { + if (paired[i]) continue; + var best: ?usize = null; + var best_d2: i128 = cert_r2 + 1; + for (odd.items[i + 1 ..], i + 1..) |w, j| { + if (paired[j]) continue; + const dx: i128 = w.x - v.x; + const dy: i128 = w.y - v.y; + const d2 = dx * dx + dy * dy; + if (d2 >= best_d2) continue; + if (d2 > micro_r2) { + // Beyond the blind micro radius a stitch is allowed only + // along real boundary geometry: each endpoint AND the + // stitch midpoint must lie on (subdivided) input segments, + // so the stitch tracks an actual edge run — possibly across + // a polyline vertex — rather than inventing a chord. + const m: Pt = .{ .x = @divTrunc(v.x + w.x, 2), .y = @divTrunc(v.y + w.y, 2) }; + var okv = false; + var okw = false; + var okm = false; + for (all) |ev| { + if (!ev.left) continue; + if (!okv and nearSegment(v, ev.p, ev.other.p)) okv = true; + if (!okw and nearSegment(w, ev.p, ev.other.p)) okw = true; + if (!okm and nearSegment(m, ev.p, ev.other.p)) okm = true; + if (okv and okw and okm) break; + } + if (!(okv and okw and okm)) continue; + } + best_d2 = d2; + best = j; + } + const j = best orelse continue; + paired[i] = true; + paired[j] = true; + stitched_gaps += 1; + const idx = edges.items.len; + try edges.append(sa, .{ .a = v, .b = odd.items[j] }); + for ([_]Pt{ v, odd.items[j] }) |pp| { + const gop = try adj.getOrPut(pp); + if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(usize).empty; + try gop.value_ptr.append(sa, idx); + } + } + } + // (2) Walk closed loops. var out = std.ArrayList([]Pt).empty; errdefer { @@ -559,7 +954,13 @@ fn connectEdges(gpa: Allocator, all: []const *SweepEvent) ![][]Pt { break; } } - const ei = picked orelse break; + const ei = picked orelse { + open_chain_walks += 1; // dead end: the emitted chain closes on a chord + const cdx: i128 = cur.x - loop_start.x; + const cdy: i128 = cur.y - loop_start.y; + if (cdx * cdx + cdy * cdy > 32 * 32) large_open_chains += 1; + break; + }; edges.items[ei].used = true; cur = if (edges.items[ei].a.eql(cur)) edges.items[ei].b else edges.items[ei].a; if (cur.eql(loop_start)) break; // closed on a real edge @@ -631,6 +1032,59 @@ pub fn pointInEvenOdd(rings: []const []const Pt, x: i64, y: i64) bool { return inside; } +/// Even-odd containment of the DOUBLED-scale point (x2,y2) in a ring-set whose +/// coordinates are at 1× — i.e. containment of the exact rational midpoint +/// (x2/2, y2/2). Ring coordinates are doubled in the arithmetic (×2 stays in +/// i64; the crossing products promote to i128 as everywhere else). This is the +/// robust-fallback membership test: exact, no rounding. +fn pointInEvenOddScaled2(rings: []const []const Pt, x2: i64, y2: i64) bool { + var inside = false; + for (rings) |ring| { + if (ring.len < 3) continue; + var j = ring.len - 1; + for (ring, 0..) |pi, i| { + const pj = ring[j]; + j = i; + const yi = 2 * pi.y; + const yj = 2 * pj.y; + if ((yi > y2) != (yj > y2)) { + const dy: i128 = @as(i128, yj) - yi; + const lhs: i128 = (@as(i128, x2) - 2 * pi.x) * dy; + const rhs: i128 = (@as(i128, y2) - yi) * (@as(i128, 2 * pj.x) - 2 * pi.x); + if (dy > 0) { + if (lhs < rhs) inside = !inside; + } else { + if (lhs > rhs) inside = !inside; + } + } + } + } + return inside; +} + +/// True if the DOUBLED-scale point (x2,y2) lies exactly on an edge of the +/// 1×-coordinate ring-set (pointOnEdge at the rational midpoint). +fn pointOnEdgeScaled2(rings: []const []const Pt, x2: i64, y2: i64) bool { + for (rings) |ring| { + if (ring.len < 2) continue; + var j = ring.len - 1; + for (ring, 0..) |pi, i| { + const pj = ring[j]; + j = i; + const ax = 2 * pj.x; + const ay = 2 * pj.y; + const bx = 2 * pi.x; + const by = 2 * pi.y; + const cross: i128 = (@as(i128, bx) - ax) * (@as(i128, y2) - ay) - (@as(i128, by) - ay) * (@as(i128, x2) - ax); + if (cross != 0) continue; + if (x2 < @min(ax, bx) or x2 > @max(ax, bx)) continue; + if (y2 < @min(ay, by) or y2 > @max(ay, by)) continue; + return true; + } + } + return false; +} + /// True if (x,y) lies exactly on any edge of the ring-set. Used by tests to /// skip ambiguous on-boundary samples. pub fn pointOnEdge(rings: []const []const Pt, x: i64, y: i64) bool { @@ -905,6 +1359,55 @@ test "fuzz: even-odd(result) == even-odd(A) op even-odd(B), A/B cleaned via unio } } +test "fuzz: raw even-odd operands — self-overlapping rings within one operand" { + // Operands are the boxes THEMSELVES (no unionAll cleaning): rings of one + // operand may overlap in area, cross, and share collinear runs — the shape a + // baked MVT polygon presents when several of its rings run along one tile + // clip edge. Even-odd oracle: a point is inside an operand iff it is in an + // odd number of that operand's boxes. + var arena_state = std.heap.ArenaAllocator.init(testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + + var prng = std.Random.DefaultPrng.init(0xDEADFA11); + const rnd = prng.random(); + const grid: i64 = 64; + const ops = [_]Op{ .unite, .intersect, .diff, .sym_diff }; + + var trial: usize = 0; + while (trial < 1500) : (trial += 1) { + const bagA = randBoxBag(rnd, grid); + const bagB = randBoxBag(rnd, grid); + var backA: [3][1][]const Pt = undefined; + var backB: [3][1][]const Pt = undefined; + const psA = bagA.polys(&backA); + const psB = bagB.polys(&backB); + // Flatten each bag's rings into ONE even-odd operand, uncleaned. + var ringsA = std.ArrayList([]const Pt).empty; + for (psA[0..bagA.n]) |p| try ringsA.appendSlice(a, p); + var ringsB = std.ArrayList([]const Pt).empty; + for (psB[0..bagB.n]) |p| try ringsB.appendSlice(a, p); + for (ops) |op| { + const r = try compute(a, ringsA.items, ringsB.items, op); + var qy: i64 = -9 * grid; + while (qy <= 12 * grid) : (qy += 17) { + var qx: i64 = -9 * grid; + while (qx <= 12 * grid) : (qx += 17) { + if (pointOnEdge(ringsA.items, qx, qy) or pointOnEdge(ringsB.items, qx, qy) or pointOnEdge(r, qx, qy)) continue; + const want = combine(op, pointInEvenOdd(ringsA.items, qx, qy), pointInEvenOdd(ringsB.items, qx, qy)); + const got = pointInEvenOdd(r, qx, qy); + if (want != got) { + std.debug.print("RAW MISMATCH trial={} op={} at ({},{}): want={} got={}\n", .{ trial, op, qx, qy, want, got }); + std.debug.print(" A={any}\n B={any}\n R={any}\n", .{ ringsA.items, ringsB.items, r }); + return error.BooleanMismatch; + } + } + } + } + _ = arena_state.reset(.retain_capacity); + } +} + test "fuzz: union is commutative and idempotent (region equality)" { var arena_state = std.heap.ArenaAllocator.init(testing.allocator); defer arena_state.deinit(); diff --git a/src/geometry/boolean_repro_test.zig b/src/geometry/boolean_repro_test.zig new file mode 100644 index 0000000..5dfc8e3 --- /dev/null +++ b/src/geometry/boolean_repro_test.zig @@ -0,0 +1,215 @@ +// Regression fixture: the exact operands of the US2EC03M DEPVS clip in tile z8/73/98 +// (compose of US2EC03M+US3EC08M) that produced an open-ring wedge artifact. +const std = @import("std"); +const boolean = @import("boolean.zig"); +const Pt = boolean.Pt; +const S0 = [_]Pt{ .{ .x = 2179, .y = 4160 }, .{ .x = 2170, .y = 4096 }, .{ .x = 2142, .y = 4065 }, .{ .x = 2099, .y = 4049 }, .{ .x = 2043, .y = 4013 }, .{ .x = 2008, .y = 4004 }, .{ .x = 1924, .y = 4006 }, .{ .x = 1924, .y = 3715 }, .{ .x = 1990, .y = 3768 }, .{ .x = 2015, .y = 3808 }, .{ .x = 2042, .y = 3782 }, .{ .x = 2072, .y = 3789 }, .{ .x = 2085, .y = 3761 }, .{ .x = 2111, .y = 3768 }, .{ .x = 2113, .y = 3782 }, .{ .x = 2093, .y = 3778 }, .{ .x = 2086, .y = 3784 }, .{ .x = 2083, .y = 3817 }, .{ .x = 2064, .y = 3831 }, .{ .x = 2065, .y = 3843 }, .{ .x = 2109, .y = 3854 }, .{ .x = 2162, .y = 3898 }, .{ .x = 2204, .y = 3904 }, .{ .x = 2242, .y = 3973 }, .{ .x = 2253, .y = 4031 }, .{ .x = 2278, .y = 4059 }, .{ .x = 2289, .y = 4094 }, .{ .x = 2325, .y = 4128 }, .{ .x = 2342, .y = 4160 }, .{ .x = 2502, .y = 4160 }, .{ .x = 2483, .y = 4128 }, .{ .x = 2422, .y = 4085 }, .{ .x = 2426, .y = 4073 }, .{ .x = 2443, .y = 4076 }, .{ .x = 2490, .y = 4106 }, .{ .x = 2523, .y = 4150 }, .{ .x = 2539, .y = 4152 }, .{ .x = 2562, .y = 4134 }, .{ .x = 2573, .y = 4135 }, .{ .x = 2575, .y = 4149 }, .{ .x = 2566, .y = 4160 }, .{ .x = 3034, .y = 4160 }, .{ .x = 3005, .y = 4096 }, .{ .x = 2970, .y = 4068 }, .{ .x = 2955, .y = 4013 }, .{ .x = 2960, .y = 4004 }, .{ .x = 2966, .y = 4005 }, .{ .x = 2991, .y = 4031 }, .{ .x = 3020, .y = 4047 }, .{ .x = 3016, .y = 4010 }, .{ .x = 2997, .y = 3986 }, .{ .x = 3009, .y = 3973 }, .{ .x = 3010, .y = 3961 }, .{ .x = 2998, .y = 3931 }, .{ .x = 3020, .y = 3920 }, .{ .x = 3016, .y = 3895 }, .{ .x = 3022, .y = 3885 }, .{ .x = 3056, .y = 3864 }, .{ .x = 3080, .y = 3873 }, .{ .x = 3084, .y = 3858 }, .{ .x = 3073, .y = 3845 }, .{ .x = 3047, .y = 3848 }, .{ .x = 3029, .y = 3840 }, .{ .x = 3016, .y = 3844 }, .{ .x = 3006, .y = 3858 }, .{ .x = 2998, .y = 3855 }, .{ .x = 2992, .y = 3824 }, .{ .x = 3008, .y = 3819 }, .{ .x = 3018, .y = 3800 }, .{ .x = 3020, .y = 3769 }, .{ .x = 3014, .y = 3756 }, .{ .x = 2984, .y = 3742 }, .{ .x = 2988, .y = 3736 }, .{ .x = 3010, .y = 3733 }, .{ .x = 3022, .y = 3716 }, .{ .x = 2994, .y = 3717 }, .{ .x = 2954, .y = 3625 }, .{ .x = 2940, .y = 3620 }, .{ .x = 2920, .y = 3631 }, .{ .x = 2893, .y = 3633 }, .{ .x = 2878, .y = 3622 }, .{ .x = 2845, .y = 3628 }, .{ .x = 2839, .y = 3617 }, .{ .x = 2864, .y = 3594 }, .{ .x = 2902, .y = 3609 }, .{ .x = 2945, .y = 3589 }, .{ .x = 2959, .y = 3592 }, .{ .x = 2998, .y = 3651 }, .{ .x = 2997, .y = 3680 }, .{ .x = 3019, .y = 3671 }, .{ .x = 3049, .y = 3671 }, .{ .x = 3056, .y = 3678 }, .{ .x = 3064, .y = 3711 }, .{ .x = 3096, .y = 3715 }, .{ .x = 3096, .y = 3746 }, .{ .x = 3121, .y = 3744 }, .{ .x = 3135, .y = 3733 }, .{ .x = 3186, .y = 3662 }, .{ .x = 3188, .y = 3611 }, .{ .x = 3198, .y = 3590 }, .{ .x = 3228, .y = 3466 }, .{ .x = 3223, .y = 3457 }, .{ .x = 3191, .y = 3440 }, .{ .x = 3155, .y = 3394 }, .{ .x = 3135, .y = 3378 }, .{ .x = 2905, .y = 3248 }, .{ .x = 2738, .y = 3199 }, .{ .x = 2711, .y = 3186 }, .{ .x = 2669, .y = 3144 }, .{ .x = 2643, .y = 3137 }, .{ .x = 2632, .y = 3115 }, .{ .x = 2623, .y = 3111 }, .{ .x = 2608, .y = 3106 }, .{ .x = 2566, .y = 3110 }, .{ .x = 2565, .y = 3118 }, .{ .x = 2583, .y = 3137 }, .{ .x = 2582, .y = 3174 }, .{ .x = 2573, .y = 3185 }, .{ .x = 2538, .y = 3182 }, .{ .x = 2537, .y = 3170 }, .{ .x = 2556, .y = 3157 }, .{ .x = 2563, .y = 3144 }, .{ .x = 2538, .y = 3098 }, .{ .x = 2488, .y = 3106 }, .{ .x = 2475, .y = 3106 }, .{ .x = 2470, .y = 3100 }, .{ .x = 2483, .y = 3087 }, .{ .x = 2539, .y = 3081 }, .{ .x = 2543, .y = 3070 }, .{ .x = 2516, .y = 3066 }, .{ .x = 2529, .y = 3053 }, .{ .x = 2539, .y = 3052 }, .{ .x = 2562, .y = 3068 }, .{ .x = 2571, .y = 3068 }, .{ .x = 2567, .y = 3048 }, .{ .x = 2536, .y = 3024 }, .{ .x = 2539, .y = 3009 }, .{ .x = 2533, .y = 3003 }, .{ .x = 2489, .y = 2994 }, .{ .x = 2410, .y = 2961 }, .{ .x = 2366, .y = 2962 }, .{ .x = 2352, .y = 2976 }, .{ .x = 2348, .y = 3047 }, .{ .x = 2338, .y = 3056 }, .{ .x = 2334, .y = 3037 }, .{ .x = 2320, .y = 3025 }, .{ .x = 2334, .y = 2999 }, .{ .x = 2332, .y = 2976 }, .{ .x = 2310, .y = 2966 }, .{ .x = 2236, .y = 2965 }, .{ .x = 2232, .y = 2957 }, .{ .x = 2244, .y = 2948 }, .{ .x = 2308, .y = 2946 }, .{ .x = 2307, .y = 2918 }, .{ .x = 2313, .y = 2910 }, .{ .x = 2333, .y = 2916 }, .{ .x = 2361, .y = 2898 }, .{ .x = 2394, .y = 2895 }, .{ .x = 2397, .y = 2885 }, .{ .x = 2370, .y = 2838 }, .{ .x = 2352, .y = 2780 }, .{ .x = 2214, .y = 2695 }, .{ .x = 2179, .y = 2664 }, .{ .x = 2135, .y = 2511 }, .{ .x = 2071, .y = 2492 }, .{ .x = 2047, .y = 2504 }, .{ .x = 2043, .y = 2517 }, .{ .x = 2061, .y = 2546 }, .{ .x = 2062, .y = 2600 }, .{ .x = 2055, .y = 2649 }, .{ .x = 2042, .y = 2663 }, .{ .x = 2038, .y = 2654 }, .{ .x = 2046, .y = 2616 }, .{ .x = 2040, .y = 2595 }, .{ .x = 1982, .y = 2519 }, .{ .x = 1924, .y = 2490 }, .{ .x = 1924, .y = 2190 }, .{ .x = 1941, .y = 2189 }, .{ .x = 1978, .y = 2197 }, .{ .x = 2036, .y = 2225 }, .{ .x = 2097, .y = 2229 }, .{ .x = 2175, .y = 2259 }, .{ .x = 2226, .y = 2293 }, .{ .x = 2294, .y = 2352 }, .{ .x = 2312, .y = 2379 }, .{ .x = 2325, .y = 2445 }, .{ .x = 2359, .y = 2517 }, .{ .x = 2367, .y = 2559 }, .{ .x = 2374, .y = 2561 }, .{ .x = 2389, .y = 2534 }, .{ .x = 2450, .y = 2544 }, .{ .x = 2457, .y = 2539 }, .{ .x = 2426, .y = 2483 }, .{ .x = 2399, .y = 2463 }, .{ .x = 2400, .y = 2453 }, .{ .x = 2409, .y = 2451 }, .{ .x = 2479, .y = 2506 }, .{ .x = 2482, .y = 2511 }, .{ .x = 2473, .y = 2528 }, .{ .x = 2524, .y = 2556 }, .{ .x = 2552, .y = 2562 }, .{ .x = 2551, .y = 2576 }, .{ .x = 2562, .y = 2580 }, .{ .x = 2572, .y = 2561 }, .{ .x = 2571, .y = 2546 }, .{ .x = 2546, .y = 2503 }, .{ .x = 2546, .y = 2492 }, .{ .x = 2584, .y = 2481 }, .{ .x = 2599, .y = 2468 }, .{ .x = 2577, .y = 2443 }, .{ .x = 2572, .y = 2428 }, .{ .x = 2600, .y = 2408 }, .{ .x = 2624, .y = 2352 }, .{ .x = 2620, .y = 2346 }, .{ .x = 2595, .y = 2343 }, .{ .x = 2587, .y = 2311 }, .{ .x = 2557, .y = 2290 }, .{ .x = 2523, .y = 2254 }, .{ .x = 2522, .y = 2244 }, .{ .x = 2529, .y = 2242 }, .{ .x = 2567, .y = 2274 }, .{ .x = 2613, .y = 2297 }, .{ .x = 2621, .y = 2329 }, .{ .x = 2652, .y = 2326 }, .{ .x = 2664, .y = 2338 }, .{ .x = 2660, .y = 2352 }, .{ .x = 2640, .y = 2366 }, .{ .x = 2647, .y = 2384 }, .{ .x = 2625, .y = 2429 }, .{ .x = 2638, .y = 2462 }, .{ .x = 2654, .y = 2463 }, .{ .x = 2669, .y = 2455 }, .{ .x = 2673, .y = 2460 }, .{ .x = 2670, .y = 2469 }, .{ .x = 2627, .y = 2508 }, .{ .x = 2642, .y = 2524 }, .{ .x = 2644, .y = 2566 }, .{ .x = 2690, .y = 2671 }, .{ .x = 2698, .y = 2674 }, .{ .x = 2706, .y = 2668 }, .{ .x = 2712, .y = 2643 }, .{ .x = 2710, .y = 2611 }, .{ .x = 2699, .y = 2581 }, .{ .x = 2718, .y = 2583 }, .{ .x = 2730, .y = 2627 }, .{ .x = 2748, .y = 2601 }, .{ .x = 2758, .y = 2602 }, .{ .x = 2764, .y = 2607 }, .{ .x = 2760, .y = 2621 }, .{ .x = 2731, .y = 2643 }, .{ .x = 2727, .y = 2658 }, .{ .x = 2733, .y = 2665 }, .{ .x = 2771, .y = 2677 }, .{ .x = 2772, .y = 2709 }, .{ .x = 2806, .y = 2735 }, .{ .x = 2870, .y = 2852 }, .{ .x = 2891, .y = 2862 }, .{ .x = 2943, .y = 2868 }, .{ .x = 2954, .y = 2878 }, .{ .x = 2971, .y = 2915 }, .{ .x = 2983, .y = 2910 }, .{ .x = 2986, .y = 2900 }, .{ .x = 2985, .y = 2876 }, .{ .x = 2955, .y = 2789 }, .{ .x = 2948, .y = 2678 }, .{ .x = 2928, .y = 2639 }, .{ .x = 2874, .y = 2594 }, .{ .x = 2889, .y = 2544 }, .{ .x = 2895, .y = 2543 }, .{ .x = 2902, .y = 2550 }, .{ .x = 2917, .y = 2600 }, .{ .x = 2930, .y = 2607 }, .{ .x = 2972, .y = 2560 }, .{ .x = 2978, .y = 2539 }, .{ .x = 2941, .y = 2464 }, .{ .x = 2792, .y = 2250 }, .{ .x = 2755, .y = 2112 }, .{ .x = 2761, .y = 2041 }, .{ .x = 2771, .y = 2020 }, .{ .x = 2815, .y = 1969 }, .{ .x = 2822, .y = 1946 }, .{ .x = 2767, .y = 1916 }, .{ .x = 2745, .y = 1912 }, .{ .x = 2724, .y = 1924 }, .{ .x = 2686, .y = 1928 }, .{ .x = 2664, .y = 1962 }, .{ .x = 2646, .y = 1971 }, .{ .x = 2604, .y = 1974 }, .{ .x = 2567, .y = 1958 }, .{ .x = 2543, .y = 1910 }, .{ .x = 2522, .y = 1892 }, .{ .x = 2528, .y = 1859 }, .{ .x = 2477, .y = 1861 }, .{ .x = 2484, .y = 1822 }, .{ .x = 2439, .y = 1775 }, .{ .x = 2442, .y = 1767 }, .{ .x = 2461, .y = 1778 }, .{ .x = 2467, .y = 1771 }, .{ .x = 2444, .y = 1718 }, .{ .x = 2412, .y = 1700 }, .{ .x = 2340, .y = 1627 }, .{ .x = 2253, .y = 1607 }, .{ .x = 2197, .y = 1573 }, .{ .x = 2041, .y = 1438 }, .{ .x = 2032, .y = 1394 }, .{ .x = 1964, .y = 1336 }, .{ .x = 1964, .y = 1329 }, .{ .x = 1987, .y = 1328 }, .{ .x = 1993, .y = 1318 }, .{ .x = 1974, .y = 1273 }, .{ .x = 1951, .y = 1268 }, .{ .x = 1944, .y = 1259 }, .{ .x = 1960, .y = 1232 }, .{ .x = 1924, .y = 1215 }, .{ .x = 1924, .y = 1190 }, .{ .x = 1950, .y = 1158 }, .{ .x = 1947, .y = 1131 }, .{ .x = 1952, .y = 1107 }, .{ .x = 1946, .y = 1094 }, .{ .x = 1924, .y = 1083 }, .{ .x = 1924, .y = 855 }, .{ .x = 1942, .y = 835 }, .{ .x = 1944, .y = 801 }, .{ .x = 1952, .y = 798 }, .{ .x = 1962, .y = 814 }, .{ .x = 1949, .y = 908 }, .{ .x = 1950, .y = 953 }, .{ .x = 1962, .y = 975 }, .{ .x = 1994, .y = 970 }, .{ .x = 2002, .y = 978 }, .{ .x = 1991, .y = 994 }, .{ .x = 1988, .y = 1023 }, .{ .x = 1967, .y = 1048 }, .{ .x = 1966, .y = 1060 }, .{ .x = 2003, .y = 1084 }, .{ .x = 2010, .y = 1101 }, .{ .x = 2006, .y = 1130 }, .{ .x = 1974, .y = 1166 }, .{ .x = 1975, .y = 1185 }, .{ .x = 1981, .y = 1203 }, .{ .x = 2019, .y = 1237 }, .{ .x = 2028, .y = 1277 }, .{ .x = 2042, .y = 1291 }, .{ .x = 2031, .y = 1325 }, .{ .x = 2124, .y = 1363 }, .{ .x = 2136, .y = 1381 }, .{ .x = 2138, .y = 1400 }, .{ .x = 2146, .y = 1403 }, .{ .x = 2164, .y = 1355 }, .{ .x = 2166, .y = 1306 }, .{ .x = 2169, .y = 1300 }, .{ .x = 2180, .y = 1300 }, .{ .x = 2189, .y = 1322 }, .{ .x = 2189, .y = 1342 }, .{ .x = 2183, .y = 1369 }, .{ .x = 2154, .y = 1430 }, .{ .x = 2154, .y = 1447 }, .{ .x = 2164, .y = 1454 }, .{ .x = 2199, .y = 1453 }, .{ .x = 2250, .y = 1491 }, .{ .x = 2299, .y = 1501 }, .{ .x = 2305, .y = 1520 }, .{ .x = 2294, .y = 1554 }, .{ .x = 2305, .y = 1566 }, .{ .x = 2328, .y = 1541 }, .{ .x = 2348, .y = 1549 }, .{ .x = 2384, .y = 1525 }, .{ .x = 2398, .y = 1550 }, .{ .x = 2413, .y = 1559 }, .{ .x = 2423, .y = 1605 }, .{ .x = 2436, .y = 1614 }, .{ .x = 2483, .y = 1585 }, .{ .x = 2495, .y = 1547 }, .{ .x = 2508, .y = 1548 }, .{ .x = 2508, .y = 1575 }, .{ .x = 2499, .y = 1596 }, .{ .x = 2462, .y = 1618 }, .{ .x = 2487, .y = 1660 }, .{ .x = 2505, .y = 1716 }, .{ .x = 2538, .y = 1762 }, .{ .x = 2540, .y = 1776 }, .{ .x = 2527, .y = 1803 }, .{ .x = 2523, .y = 1830 }, .{ .x = 2566, .y = 1856 }, .{ .x = 2583, .y = 1884 }, .{ .x = 2591, .y = 1885 }, .{ .x = 2600, .y = 1868 }, .{ .x = 2631, .y = 1853 }, .{ .x = 2679, .y = 1872 }, .{ .x = 2693, .y = 1869 }, .{ .x = 2725, .y = 1809 }, .{ .x = 2779, .y = 1732 }, .{ .x = 2786, .y = 1701 }, .{ .x = 2776, .y = 1663 }, .{ .x = 2800, .y = 1637 }, .{ .x = 2802, .y = 1622 }, .{ .x = 2751, .y = 1604 }, .{ .x = 2743, .y = 1596 }, .{ .x = 2700, .y = 1516 }, .{ .x = 2583, .y = 1378 }, .{ .x = 2486, .y = 1275 }, .{ .x = 2427, .y = 1149 }, .{ .x = 2406, .y = 1059 }, .{ .x = 2414, .y = 990 }, .{ .x = 2409, .y = 873 }, .{ .x = 2424, .y = 779 }, .{ .x = 2376, .y = 604 }, .{ .x = 2365, .y = 534 }, .{ .x = 2361, .y = 458 }, .{ .x = 2382, .y = 369 }, .{ .x = 2377, .y = 356 }, .{ .x = 2328, .y = 350 }, .{ .x = 2306, .y = 334 }, .{ .x = 2292, .y = 295 }, .{ .x = 2300, .y = 262 }, .{ .x = 2284, .y = 226 }, .{ .x = 2290, .y = 201 }, .{ .x = 2358, .y = 165 }, .{ .x = 2389, .y = 106 }, .{ .x = 2432, .y = 75 }, .{ .x = 2457, .y = 43 }, .{ .x = 2487, .y = -64 }, .{ .x = 2371, .y = -64 }, .{ .x = 2383, .y = -46 }, .{ .x = 2372, .y = -36 }, .{ .x = 2353, .y = -44 }, .{ .x = 2347, .y = -64 }, .{ .x = 3335, .y = -64 }, .{ .x = 3349, .y = -15 }, .{ .x = 3329, .y = 66 }, .{ .x = 3396, .y = 187 }, .{ .x = 3409, .y = 198 }, .{ .x = 3476, .y = 165 }, .{ .x = 3479, .y = 173 }, .{ .x = 3447, .y = 200 }, .{ .x = 3425, .y = 249 }, .{ .x = 3418, .y = 255 }, .{ .x = 3395, .y = 250 }, .{ .x = 3362, .y = 197 }, .{ .x = 3296, .y = 143 }, .{ .x = 3285, .y = 126 }, .{ .x = 3298, .y = 64 }, .{ .x = 3264, .y = 17 }, .{ .x = 3238, .y = 5 }, .{ .x = 3201, .y = 7 }, .{ .x = 3193, .y = -4 }, .{ .x = 3190, .y = -28 }, .{ .x = 3168, .y = -64 }, .{ .x = 3092, .y = -64 }, .{ .x = 3095, .y = -39 }, .{ .x = 3090, .y = -30 }, .{ .x = 3049, .y = -26 }, .{ .x = 3033, .y = -16 }, .{ .x = 3020, .y = 10 }, .{ .x = 2990, .y = 120 }, .{ .x = 2938, .y = 188 }, .{ .x = 2953, .y = 205 }, .{ .x = 2954, .y = 215 }, .{ .x = 2925, .y = 236 }, .{ .x = 2920, .y = 246 }, .{ .x = 2915, .y = 286 }, .{ .x = 2923, .y = 372 }, .{ .x = 2907, .y = 430 }, .{ .x = 2902, .y = 480 }, .{ .x = 2918, .y = 567 }, .{ .x = 2929, .y = 564 }, .{ .x = 2941, .y = 540 }, .{ .x = 2982, .y = 528 }, .{ .x = 2981, .y = 503 }, .{ .x = 2952, .y = 456 }, .{ .x = 2955, .y = 426 }, .{ .x = 2975, .y = 371 }, .{ .x = 3004, .y = 340 }, .{ .x = 3003, .y = 273 }, .{ .x = 2996, .y = 237 }, .{ .x = 3000, .y = 225 }, .{ .x = 3022, .y = 206 }, .{ .x = 3048, .y = 196 }, .{ .x = 3056, .y = 200 }, .{ .x = 3026, .y = 268 }, .{ .x = 3046, .y = 312 }, .{ .x = 3039, .y = 373 }, .{ .x = 3050, .y = 392 }, .{ .x = 3093, .y = 398 }, .{ .x = 3113, .y = 425 }, .{ .x = 3123, .y = 426 }, .{ .x = 3131, .y = 369 }, .{ .x = 3172, .y = 318 }, .{ .x = 3154, .y = 285 }, .{ .x = 3164, .y = 273 }, .{ .x = 3201, .y = 275 }, .{ .x = 3219, .y = 255 }, .{ .x = 3232, .y = 270 }, .{ .x = 3292, .y = 298 }, .{ .x = 3312, .y = 316 }, .{ .x = 3313, .y = 327 }, .{ .x = 3303, .y = 338 }, .{ .x = 3231, .y = 307 }, .{ .x = 3215, .y = 316 }, .{ .x = 3217, .y = 408 }, .{ .x = 3232, .y = 442 }, .{ .x = 3267, .y = 492 }, .{ .x = 3324, .y = 558 }, .{ .x = 3334, .y = 565 }, .{ .x = 3343, .y = 560 }, .{ .x = 3401, .y = 433 }, .{ .x = 3414, .y = 427 }, .{ .x = 3446, .y = 442 }, .{ .x = 3470, .y = 445 }, .{ .x = 3466, .y = 457 }, .{ .x = 3405, .y = 472 }, .{ .x = 3389, .y = 505 }, .{ .x = 3396, .y = 549 }, .{ .x = 3446, .y = 620 }, .{ .x = 3471, .y = 684 }, .{ .x = 3500, .y = 707 }, .{ .x = 3550, .y = 721 }, .{ .x = 3586, .y = 785 }, .{ .x = 3603, .y = 787 }, .{ .x = 3633, .y = 763 }, .{ .x = 3732, .y = 803 }, .{ .x = 3769, .y = 869 }, .{ .x = 3783, .y = 875 }, .{ .x = 3808, .y = 874 }, .{ .x = 3819, .y = 904 }, .{ .x = 3831, .y = 911 }, .{ .x = 3842, .y = 907 }, .{ .x = 3861, .y = 864 }, .{ .x = 3923, .y = 841 }, .{ .x = 3948, .y = 811 }, .{ .x = 3953, .y = 792 }, .{ .x = 3938, .y = 724 }, .{ .x = 3971, .y = 686 }, .{ .x = 4027, .y = 649 }, .{ .x = 4042, .y = 576 }, .{ .x = 4051, .y = 561 }, .{ .x = 4058, .y = 563 }, .{ .x = 4052, .y = 632 }, .{ .x = 4044, .y = 656 }, .{ .x = 4027, .y = 696 }, .{ .x = 4001, .y = 713 }, .{ .x = 3986, .y = 748 }, .{ .x = 3987, .y = 768 }, .{ .x = 4000, .y = 792 }, .{ .x = 3982, .y = 816 }, .{ .x = 3974, .y = 885 }, .{ .x = 3947, .y = 899 }, .{ .x = 3913, .y = 891 }, .{ .x = 3869, .y = 930 }, .{ .x = 3857, .y = 949 }, .{ .x = 3806, .y = 964 }, .{ .x = 3779, .y = 961 }, .{ .x = 3731, .y = 937 }, .{ .x = 3707, .y = 909 }, .{ .x = 3688, .y = 873 }, .{ .x = 3664, .y = 854 }, .{ .x = 3636, .y = 863 }, .{ .x = 3607, .y = 894 }, .{ .x = 3596, .y = 898 }, .{ .x = 3579, .y = 895 }, .{ .x = 3552, .y = 869 }, .{ .x = 3533, .y = 836 }, .{ .x = 3518, .y = 827 }, .{ .x = 3497, .y = 830 }, .{ .x = 3458, .y = 861 }, .{ .x = 3447, .y = 860 }, .{ .x = 3421, .y = 830 }, .{ .x = 3406, .y = 823 }, .{ .x = 3422, .y = 789 }, .{ .x = 3392, .y = 760 }, .{ .x = 3380, .y = 758 }, .{ .x = 3369, .y = 764 }, .{ .x = 3322, .y = 819 }, .{ .x = 3310, .y = 828 }, .{ .x = 3297, .y = 828 }, .{ .x = 3275, .y = 805 }, .{ .x = 3234, .y = 800 }, .{ .x = 3231, .y = 793 }, .{ .x = 3248, .y = 751 }, .{ .x = 3239, .y = 725 }, .{ .x = 3227, .y = 725 }, .{ .x = 3188, .y = 746 }, .{ .x = 3166, .y = 798 }, .{ .x = 3156, .y = 801 }, .{ .x = 3142, .y = 796 }, .{ .x = 3123, .y = 755 }, .{ .x = 3103, .y = 731 }, .{ .x = 3085, .y = 722 }, .{ .x = 3083, .y = 733 }, .{ .x = 3113, .y = 773 }, .{ .x = 3123, .y = 814 }, .{ .x = 3157, .y = 832 }, .{ .x = 3136, .y = 885 }, .{ .x = 3127, .y = 894 }, .{ .x = 3115, .y = 903 }, .{ .x = 3090, .y = 892 }, .{ .x = 3075, .y = 894 }, .{ .x = 3058, .y = 917 }, .{ .x = 3060, .y = 938 }, .{ .x = 3090, .y = 976 }, .{ .x = 3107, .y = 1011 }, .{ .x = 3112, .y = 1078 }, .{ .x = 3126, .y = 1064 }, .{ .x = 3134, .y = 965 }, .{ .x = 3139, .y = 958 }, .{ .x = 3149, .y = 965 }, .{ .x = 3151, .y = 1042 }, .{ .x = 3160, .y = 1057 }, .{ .x = 3171, .y = 1047 }, .{ .x = 3183, .y = 972 }, .{ .x = 3189, .y = 964 }, .{ .x = 3201, .y = 963 }, .{ .x = 3203, .y = 971 }, .{ .x = 3198, .y = 1044 }, .{ .x = 3273, .y = 992 }, .{ .x = 3288, .y = 935 }, .{ .x = 3312, .y = 939 }, .{ .x = 3316, .y = 946 }, .{ .x = 3306, .y = 975 }, .{ .x = 3316, .y = 980 }, .{ .x = 3368, .y = 965 }, .{ .x = 3383, .y = 955 }, .{ .x = 3399, .y = 929 }, .{ .x = 3406, .y = 929 }, .{ .x = 3413, .y = 939 }, .{ .x = 3397, .y = 960 }, .{ .x = 3363, .y = 993 }, .{ .x = 3323, .y = 1015 }, .{ .x = 3320, .y = 1029 }, .{ .x = 3348, .y = 1080 }, .{ .x = 3372, .y = 1097 }, .{ .x = 3445, .y = 1097 }, .{ .x = 3417, .y = 1112 }, .{ .x = 3379, .y = 1117 }, .{ .x = 3336, .y = 1095 }, .{ .x = 3312, .y = 1069 }, .{ .x = 3300, .y = 1069 }, .{ .x = 3320, .y = 1130 }, .{ .x = 3323, .y = 1165 }, .{ .x = 3299, .y = 1180 }, .{ .x = 3280, .y = 1179 }, .{ .x = 3275, .y = 1162 }, .{ .x = 3286, .y = 1119 }, .{ .x = 3278, .y = 1113 }, .{ .x = 3268, .y = 1111 }, .{ .x = 3257, .y = 1124 }, .{ .x = 3252, .y = 1160 }, .{ .x = 3234, .y = 1134 }, .{ .x = 3216, .y = 1128 }, .{ .x = 3205, .y = 1154 }, .{ .x = 3203, .y = 1218 }, .{ .x = 3186, .y = 1225 }, .{ .x = 3163, .y = 1203 }, .{ .x = 3148, .y = 1203 }, .{ .x = 3127, .y = 1254 }, .{ .x = 3098, .y = 1298 }, .{ .x = 3090, .y = 1336 }, .{ .x = 3075, .y = 1331 }, .{ .x = 3075, .y = 1315 }, .{ .x = 3082, .y = 1291 }, .{ .x = 3109, .y = 1249 }, .{ .x = 3114, .y = 1230 }, .{ .x = 3109, .y = 1209 }, .{ .x = 3076, .y = 1199 }, .{ .x = 3046, .y = 1216 }, .{ .x = 3023, .y = 1218 }, .{ .x = 2999, .y = 1193 }, .{ .x = 2986, .y = 1188 }, .{ .x = 2978, .y = 1196 }, .{ .x = 2983, .y = 1238 }, .{ .x = 2978, .y = 1247 }, .{ .x = 2963, .y = 1250 }, .{ .x = 2948, .y = 1232 }, .{ .x = 2936, .y = 1131 }, .{ .x = 2928, .y = 1113 }, .{ .x = 2939, .y = 1107 }, .{ .x = 2952, .y = 1122 }, .{ .x = 2957, .y = 1109 }, .{ .x = 2946, .y = 1087 }, .{ .x = 2927, .y = 1081 }, .{ .x = 2914, .y = 1086 }, .{ .x = 2911, .y = 1105 }, .{ .x = 2916, .y = 1156 }, .{ .x = 2941, .y = 1269 }, .{ .x = 2975, .y = 1339 }, .{ .x = 3006, .y = 1377 }, .{ .x = 3049, .y = 1455 }, .{ .x = 3098, .y = 1567 }, .{ .x = 3121, .y = 1638 }, .{ .x = 3153, .y = 1661 }, .{ .x = 3210, .y = 1742 }, .{ .x = 3234, .y = 1753 }, .{ .x = 3244, .y = 1749 }, .{ .x = 3256, .y = 1727 }, .{ .x = 3237, .y = 1662 }, .{ .x = 3251, .y = 1636 }, .{ .x = 3253, .y = 1614 }, .{ .x = 3272, .y = 1601 }, .{ .x = 3296, .y = 1608 }, .{ .x = 3306, .y = 1645 }, .{ .x = 3295, .y = 1714 }, .{ .x = 3325, .y = 1716 }, .{ .x = 3379, .y = 1755 }, .{ .x = 3395, .y = 1781 }, .{ .x = 3401, .y = 1810 }, .{ .x = 3424, .y = 1820 }, .{ .x = 3434, .y = 1833 }, .{ .x = 3435, .y = 1847 }, .{ .x = 3428, .y = 1849 }, .{ .x = 3378, .y = 1820 }, .{ .x = 3355, .y = 1820 }, .{ .x = 3354, .y = 1843 }, .{ .x = 3331, .y = 1846 }, .{ .x = 3339, .y = 1874 }, .{ .x = 3352, .y = 1884 }, .{ .x = 3386, .y = 1874 }, .{ .x = 3515, .y = 1920 }, .{ .x = 3518, .y = 1929 }, .{ .x = 3504, .y = 1949 }, .{ .x = 3501, .y = 1983 }, .{ .x = 3476, .y = 2021 }, .{ .x = 3480, .y = 2035 }, .{ .x = 3542, .y = 2035 }, .{ .x = 3566, .y = 2049 }, .{ .x = 3572, .y = 2047 }, .{ .x = 3535, .y = 1993 }, .{ .x = 3531, .y = 1967 }, .{ .x = 3535, .y = 1957 }, .{ .x = 3559, .y = 1940 }, .{ .x = 3592, .y = 1947 }, .{ .x = 3579, .y = 1971 }, .{ .x = 3582, .y = 1985 }, .{ .x = 3619, .y = 1982 }, .{ .x = 3626, .y = 1994 }, .{ .x = 3620, .y = 2019 }, .{ .x = 3648, .y = 2021 }, .{ .x = 3657, .y = 2038 }, .{ .x = 3682, .y = 2035 }, .{ .x = 3673, .y = 2062 }, .{ .x = 3689, .y = 2083 }, .{ .x = 3686, .y = 2096 }, .{ .x = 3658, .y = 2111 }, .{ .x = 3622, .y = 2103 }, .{ .x = 3626, .y = 2120 }, .{ .x = 3651, .y = 2133 }, .{ .x = 3698, .y = 2137 }, .{ .x = 3721, .y = 2152 }, .{ .x = 3751, .y = 2204 }, .{ .x = 3782, .y = 2224 }, .{ .x = 3815, .y = 2271 }, .{ .x = 3825, .y = 2264 }, .{ .x = 3813, .y = 2181 }, .{ .x = 3780, .y = 2131 }, .{ .x = 3818, .y = 2122 }, .{ .x = 3838, .y = 2096 }, .{ .x = 3838, .y = 2086 }, .{ .x = 3810, .y = 2062 }, .{ .x = 3829, .y = 2049 }, .{ .x = 3840, .y = 2017 }, .{ .x = 3813, .y = 1967 }, .{ .x = 3799, .y = 1956 }, .{ .x = 3761, .y = 1945 }, .{ .x = 3759, .y = 1935 }, .{ .x = 3786, .y = 1897 }, .{ .x = 3857, .y = 1833 }, .{ .x = 3873, .y = 1807 }, .{ .x = 3887, .y = 1773 }, .{ .x = 3883, .y = 1711 }, .{ .x = 3890, .y = 1697 }, .{ .x = 3897, .y = 1689 }, .{ .x = 3911, .y = 1688 }, .{ .x = 3999, .y = 1724 }, .{ .x = 4019, .y = 1764 }, .{ .x = 4036, .y = 1780 }, .{ .x = 3999, .y = 1852 }, .{ .x = 3965, .y = 1891 }, .{ .x = 3930, .y = 1907 }, .{ .x = 3881, .y = 1912 }, .{ .x = 3862, .y = 1924 }, .{ .x = 3973, .y = 2015 }, .{ .x = 3974, .y = 2067 }, .{ .x = 4014, .y = 2113 }, .{ .x = 4018, .y = 2129 }, .{ .x = 4006, .y = 2167 }, .{ .x = 4007, .y = 2183 }, .{ .x = 4016, .y = 2183 }, .{ .x = 4067, .y = 2137 }, .{ .x = 4055, .y = 2081 }, .{ .x = 4081, .y = 2069 }, .{ .x = 4074, .y = 2040 }, .{ .x = 4076, .y = 1962 }, .{ .x = 4089, .y = 1935 }, .{ .x = 4160, .y = 1856 }, .{ .x = 4160, .y = 2327 }, .{ .x = 4127, .y = 2355 }, .{ .x = 4100, .y = 2348 }, .{ .x = 4078, .y = 2351 }, .{ .x = 4059, .y = 2361 }, .{ .x = 4046, .y = 2382 }, .{ .x = 4060, .y = 2411 }, .{ .x = 4103, .y = 2444 }, .{ .x = 4102, .y = 2451 }, .{ .x = 4072, .y = 2441 }, .{ .x = 4057, .y = 2451 }, .{ .x = 4048, .y = 2496 }, .{ .x = 4048, .y = 2546 }, .{ .x = 4055, .y = 2561 }, .{ .x = 4071, .y = 2569 }, .{ .x = 4100, .y = 2567 }, .{ .x = 4126, .y = 2573 }, .{ .x = 4131, .y = 2566 }, .{ .x = 4121, .y = 2517 }, .{ .x = 4128, .y = 2513 }, .{ .x = 4160, .y = 2520 }, .{ .x = 4160, .y = 4160 } }; +const S1 = [_]Pt{ .{ .x = 3921, .y = 3609 }, .{ .x = 3911, .y = 3660 }, .{ .x = 3918, .y = 3742 }, .{ .x = 3934, .y = 3768 }, .{ .x = 3969, .y = 3784 }, .{ .x = 3973, .y = 3769 }, .{ .x = 3943, .y = 3749 }, .{ .x = 3944, .y = 3738 }, .{ .x = 3962, .y = 3712 }, .{ .x = 3945, .y = 3680 }, .{ .x = 3963, .y = 3659 }, .{ .x = 3938, .y = 3619 }, .{ .x = 3921, .y = 3609 } }; +const S2 = [_]Pt{ .{ .x = 3973, .y = 3700 }, .{ .x = 3993, .y = 3684 }, .{ .x = 3985, .y = 3664 }, .{ .x = 3968, .y = 3680 }, .{ .x = 3966, .y = 3690 }, .{ .x = 3973, .y = 3700 } }; +const S3 = [_]Pt{ .{ .x = 3921, .y = 3577 }, .{ .x = 3934, .y = 3576 }, .{ .x = 3935, .y = 3562 }, .{ .x = 3907, .y = 3529 }, .{ .x = 3888, .y = 3521 }, .{ .x = 3884, .y = 3528 }, .{ .x = 3894, .y = 3552 }, .{ .x = 3921, .y = 3577 } }; +const S4 = [_]Pt{ .{ .x = 3849, .y = 3440 }, .{ .x = 3839, .y = 3409 }, .{ .x = 3822, .y = 3409 }, .{ .x = 3823, .y = 3425 }, .{ .x = 3840, .y = 3458 }, .{ .x = 3857, .y = 3460 }, .{ .x = 3849, .y = 3440 } }; +const S5 = [_]Pt{ .{ .x = 3859, .y = 3268 }, .{ .x = 3864, .y = 3226 }, .{ .x = 3838, .y = 3201 }, .{ .x = 3829, .y = 3182 }, .{ .x = 3843, .y = 3155 }, .{ .x = 3849, .y = 3090 }, .{ .x = 3829, .y = 3059 }, .{ .x = 3806, .y = 3051 }, .{ .x = 3792, .y = 3054 }, .{ .x = 3773, .y = 3068 }, .{ .x = 3768, .y = 3083 }, .{ .x = 3773, .y = 3197 }, .{ .x = 3782, .y = 3248 }, .{ .x = 3791, .y = 3254 }, .{ .x = 3803, .y = 3247 }, .{ .x = 3803, .y = 3220 }, .{ .x = 3786, .y = 3178 }, .{ .x = 3796, .y = 3171 }, .{ .x = 3805, .y = 3176 }, .{ .x = 3830, .y = 3250 }, .{ .x = 3831, .y = 3279 }, .{ .x = 3816, .y = 3311 }, .{ .x = 3826, .y = 3348 }, .{ .x = 3840, .y = 3346 }, .{ .x = 3847, .y = 3328 }, .{ .x = 3866, .y = 3321 }, .{ .x = 3859, .y = 3268 } }; +const S6 = [_]Pt{ .{ .x = 3892, .y = 3092 }, .{ .x = 3906, .y = 3141 }, .{ .x = 3919, .y = 3155 }, .{ .x = 3932, .y = 3157 }, .{ .x = 3942, .y = 3122 }, .{ .x = 3972, .y = 3088 }, .{ .x = 3969, .y = 3029 }, .{ .x = 3958, .y = 3004 }, .{ .x = 3954, .y = 2968 }, .{ .x = 3904, .y = 2963 }, .{ .x = 3881, .y = 2953 }, .{ .x = 3878, .y = 2937 }, .{ .x = 3893, .y = 2913 }, .{ .x = 3880, .y = 2904 }, .{ .x = 3865, .y = 2905 }, .{ .x = 3795, .y = 2940 }, .{ .x = 3778, .y = 2970 }, .{ .x = 3771, .y = 3028 }, .{ .x = 3778, .y = 3046 }, .{ .x = 3783, .y = 3046 }, .{ .x = 3805, .y = 3023 }, .{ .x = 3852, .y = 3016 }, .{ .x = 3870, .y = 3041 }, .{ .x = 3875, .y = 3070 }, .{ .x = 3892, .y = 3092 } }; +const S7 = [_]Pt{ .{ .x = 3917, .y = 3174 }, .{ .x = 3891, .y = 3144 }, .{ .x = 3880, .y = 3099 }, .{ .x = 3867, .y = 3094 }, .{ .x = 3859, .y = 3167 }, .{ .x = 3903, .y = 3248 }, .{ .x = 3912, .y = 3260 }, .{ .x = 3920, .y = 3260 }, .{ .x = 3944, .y = 3202 }, .{ .x = 3942, .y = 3192 }, .{ .x = 3917, .y = 3174 } }; +const S8 = [_]Pt{ .{ .x = 3841, .y = 2752 }, .{ .x = 3858, .y = 2765 }, .{ .x = 3867, .y = 2790 }, .{ .x = 3879, .y = 2789 }, .{ .x = 3895, .y = 2769 }, .{ .x = 3890, .y = 2714 }, .{ .x = 3873, .y = 2683 }, .{ .x = 3861, .y = 2641 }, .{ .x = 3881, .y = 2581 }, .{ .x = 3878, .y = 2571 }, .{ .x = 3860, .y = 2565 }, .{ .x = 3848, .y = 2566 }, .{ .x = 3821, .y = 2589 }, .{ .x = 3775, .y = 2641 }, .{ .x = 3773, .y = 2655 }, .{ .x = 3809, .y = 2698 }, .{ .x = 3804, .y = 2714 }, .{ .x = 3786, .y = 2718 }, .{ .x = 3744, .y = 2700 }, .{ .x = 3736, .y = 2708 }, .{ .x = 3815, .y = 2777 }, .{ .x = 3827, .y = 2773 }, .{ .x = 3828, .y = 2751 }, .{ .x = 3841, .y = 2752 } }; +const S9 = [_]Pt{ .{ .x = 3662, .y = 2576 }, .{ .x = 3649, .y = 2556 }, .{ .x = 3640, .y = 2562 }, .{ .x = 3644, .y = 2616 }, .{ .x = 3654, .y = 2627 }, .{ .x = 3673, .y = 2620 }, .{ .x = 3674, .y = 2602 }, .{ .x = 3662, .y = 2576 } }; +const S10 = [_]Pt{ .{ .x = 4085, .y = 2656 }, .{ .x = 4096, .y = 2655 }, .{ .x = 4097, .y = 2639 }, .{ .x = 4089, .y = 2610 }, .{ .x = 4081, .y = 2604 }, .{ .x = 4070, .y = 2618 }, .{ .x = 4085, .y = 2656 } }; +const S11 = [_]Pt{ .{ .x = 3668, .y = 2517 }, .{ .x = 3666, .y = 2494 }, .{ .x = 3654, .y = 2487 }, .{ .x = 3646, .y = 2496 }, .{ .x = 3647, .y = 2528 }, .{ .x = 3661, .y = 2547 }, .{ .x = 3667, .y = 2547 }, .{ .x = 3668, .y = 2517 } }; +const S12 = [_]Pt{ .{ .x = 3788, .y = 2513 }, .{ .x = 3813, .y = 2512 }, .{ .x = 3828, .y = 2499 }, .{ .x = 3828, .y = 2480 }, .{ .x = 3809, .y = 2457 }, .{ .x = 3810, .y = 2446 }, .{ .x = 3847, .y = 2415 }, .{ .x = 3846, .y = 2394 }, .{ .x = 3823, .y = 2374 }, .{ .x = 3787, .y = 2371 }, .{ .x = 3789, .y = 2362 }, .{ .x = 3803, .y = 2353 }, .{ .x = 3803, .y = 2342 }, .{ .x = 3769, .y = 2302 }, .{ .x = 3711, .y = 2299 }, .{ .x = 3707, .y = 2311 }, .{ .x = 3746, .y = 2329 }, .{ .x = 3749, .y = 2342 }, .{ .x = 3719, .y = 2358 }, .{ .x = 3656, .y = 2344 }, .{ .x = 3653, .y = 2353 }, .{ .x = 3668, .y = 2378 }, .{ .x = 3669, .y = 2393 }, .{ .x = 3657, .y = 2427 }, .{ .x = 3659, .y = 2462 }, .{ .x = 3669, .y = 2476 }, .{ .x = 3687, .y = 2475 }, .{ .x = 3723, .y = 2431 }, .{ .x = 3733, .y = 2430 }, .{ .x = 3740, .y = 2439 }, .{ .x = 3753, .y = 2502 }, .{ .x = 3767, .y = 2511 }, .{ .x = 3788, .y = 2513 } }; +const S13 = [_]Pt{ .{ .x = 3435, .y = 2113 }, .{ .x = 3406, .y = 2067 }, .{ .x = 3400, .y = 2035 }, .{ .x = 3430, .y = 1984 }, .{ .x = 3429, .y = 1971 }, .{ .x = 3415, .y = 1959 }, .{ .x = 3383, .y = 1969 }, .{ .x = 3333, .y = 1971 }, .{ .x = 3332, .y = 1986 }, .{ .x = 3365, .y = 2051 }, .{ .x = 3388, .y = 2124 }, .{ .x = 3398, .y = 2137 }, .{ .x = 3469, .y = 2171 }, .{ .x = 3507, .y = 2213 }, .{ .x = 3529, .y = 2212 }, .{ .x = 3538, .y = 2204 }, .{ .x = 3540, .y = 2180 }, .{ .x = 3507, .y = 2154 }, .{ .x = 3460, .y = 2142 }, .{ .x = 3435, .y = 2113 } }; +const S14 = [_]Pt{ .{ .x = 3283, .y = 1910 }, .{ .x = 3259, .y = 1865 }, .{ .x = 3259, .y = 1856 }, .{ .x = 3280, .y = 1833 }, .{ .x = 3276, .y = 1804 }, .{ .x = 3251, .y = 1776 }, .{ .x = 3230, .y = 1775 }, .{ .x = 3221, .y = 1800 }, .{ .x = 3232, .y = 1853 }, .{ .x = 3246, .y = 1888 }, .{ .x = 3279, .y = 1936 }, .{ .x = 3288, .y = 1946 }, .{ .x = 3299, .y = 1946 }, .{ .x = 3300, .y = 1941 }, .{ .x = 3283, .y = 1910 } }; +const S15 = [_]Pt{ .{ .x = 3186, .y = 1885 }, .{ .x = 3191, .y = 1876 }, .{ .x = 3181, .y = 1820 }, .{ .x = 3166, .y = 1761 }, .{ .x = 3150, .y = 1734 }, .{ .x = 3144, .y = 1746 }, .{ .x = 3153, .y = 1840 }, .{ .x = 3167, .y = 1876 }, .{ .x = 3186, .y = 1885 } }; +const S16 = [_]Pt{ .{ .x = 2833, .y = 208 }, .{ .x = 2832, .y = 186 }, .{ .x = 2853, .y = 198 }, .{ .x = 2862, .y = 188 }, .{ .x = 2869, .y = 114 }, .{ .x = 2851, .y = 98 }, .{ .x = 2840, .y = 102 }, .{ .x = 2835, .y = 130 }, .{ .x = 2806, .y = 160 }, .{ .x = 2777, .y = 208 }, .{ .x = 2780, .y = 258 }, .{ .x = 2804, .y = 297 }, .{ .x = 2813, .y = 299 }, .{ .x = 2839, .y = 294 }, .{ .x = 2854, .y = 284 }, .{ .x = 2856, .y = 276 }, .{ .x = 2851, .y = 256 }, .{ .x = 2818, .y = 248 }, .{ .x = 2810, .y = 236 }, .{ .x = 2809, .y = 229 }, .{ .x = 2831, .y = 218 }, .{ .x = 2833, .y = 208 } }; +const S17 = [_]Pt{ .{ .x = 2815, .y = -64 }, .{ .x = 2836, .y = -52 }, .{ .x = 2844, .y = -53 }, .{ .x = 2849, .y = -64 } }; +const S18 = [_]Pt{ .{ .x = 2541, .y = 2675 }, .{ .x = 2553, .y = 2671 }, .{ .x = 2558, .y = 2657 }, .{ .x = 2548, .y = 2623 }, .{ .x = 2490, .y = 2584 }, .{ .x = 2476, .y = 2564 }, .{ .x = 2461, .y = 2562 }, .{ .x = 2510, .y = 2623 }, .{ .x = 2526, .y = 2669 }, .{ .x = 2541, .y = 2675 } }; +const F0 = [_]Pt{ .{ .x = 4160, .y = -64 }, .{ .x = 4160, .y = 4160 }, .{ .x = 1924, .y = 4160 }, .{ .x = 1924, .y = 4006 }, .{ .x = 1924, .y = 3715 }, .{ .x = 1924, .y = 3035 }, .{ .x = 1924, .y = 2490 }, .{ .x = 1924, .y = 2190 }, .{ .x = 1924, .y = 1215 }, .{ .x = 1924, .y = 1191 }, .{ .x = 1924, .y = 1083 }, .{ .x = 1924, .y = 855 }, .{ .x = 1924, .y = -64 } }; +const SUBJECT = [_][]const Pt{ &S0, &S1, &S2, &S3, &S4, &S5, &S6, &S7, &S8, &S9, &S10, &S11, &S12, &S13, &S14, &S15, &S16, &S17, &S18 }; +const FACE = [_][]const Pt{&F0}; + +test "compose clip repro: wedge points stay inside subject-and-face intersection" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const r = try boolean.compute(a, &SUBJECT, &FACE, .intersect); + // Points in the open bay water (inside S0, inside no hole, inside the face box): + // these MUST be in the intersection. The wedge artifact excluded them. + const probes = [_]Pt{ + .{ .x = 2794, .y = 45 }, + .{ .x = 2740, .y = 150 }, + .{ .x = 2600, .y = 300 }, + .{ .x = 2900, .y = 200 }, + }; + for (probes) |q| { + try std.testing.expect(boolean.pointInEvenOdd(&SUBJECT, q.x, q.y)); // sanity: in subject + try std.testing.expect(boolean.pointInEvenOdd(&FACE, q.x, q.y)); // sanity: in face + if (!boolean.pointInEvenOdd(r, q.x, q.y)) { + std.debug.print("wedge point ({d},{d}) missing from intersection\n", .{ q.x, q.y }); + return error.WedgeExcluded; + } + } +} + +const LI_S0 = [_]Pt{ .{ .x = 4030, .y = -64 }, .{ .x = 4030, .y = -42 }, .{ .x = 4022, .y = -44 }, .{ .x = 4009, .y = -64 }, .{ .x = 3621, .y = -64 }, .{ .x = 3615, .y = -59 }, .{ .x = 3565, .y = -64 }, .{ .x = 3572, .y = -53 }, .{ .x = 3594, .y = -46 }, .{ .x = 3594, .y = -39 }, .{ .x = 3561, .y = -18 }, .{ .x = 3514, .y = -17 }, .{ .x = 3494, .y = 2 }, .{ .x = 3486, .y = 2 }, .{ .x = 3476, .y = -17 }, .{ .x = 3469, .y = -17 }, .{ .x = 3463, .y = 10 }, .{ .x = 3407, .y = 86 }, .{ .x = 3371, .y = 160 }, .{ .x = 3337, .y = 197 }, .{ .x = 3306, .y = 246 }, .{ .x = 3271, .y = 254 }, .{ .x = 3254, .y = 269 }, .{ .x = 3237, .y = 315 }, .{ .x = 3217, .y = 337 }, .{ .x = 3205, .y = 343 }, .{ .x = 3148, .y = 348 }, .{ .x = 3123, .y = 371 }, .{ .x = 3115, .y = 371 }, .{ .x = 3054, .y = 343 }, .{ .x = 3030, .y = 358 }, .{ .x = 3016, .y = 359 }, .{ .x = 3012, .y = 319 }, .{ .x = 3004, .y = 318 }, .{ .x = 2985, .y = 375 }, .{ .x = 2975, .y = 377 }, .{ .x = 2967, .y = 336 }, .{ .x = 2956, .y = 343 }, .{ .x = 2959, .y = 380 }, .{ .x = 2951, .y = 368 }, .{ .x = 2909, .y = 366 }, .{ .x = 2907, .y = 372 }, .{ .x = 2920, .y = 384 }, .{ .x = 2965, .y = 397 }, .{ .x = 2965, .y = 405 }, .{ .x = 2941, .y = 430 }, .{ .x = 2906, .y = 431 }, .{ .x = 2858, .y = 471 }, .{ .x = 2746, .y = 469 }, .{ .x = 2721, .y = 479 }, .{ .x = 2761, .y = 484 }, .{ .x = 2811, .y = 475 }, .{ .x = 2859, .y = 485 }, .{ .x = 2923, .y = 457 }, .{ .x = 2937, .y = 459 }, .{ .x = 2943, .y = 482 }, .{ .x = 2910, .y = 514 }, .{ .x = 2948, .y = 529 }, .{ .x = 2957, .y = 558 }, .{ .x = 2965, .y = 564 }, .{ .x = 2986, .y = 541 }, .{ .x = 2994, .y = 503 }, .{ .x = 3029, .y = 491 }, .{ .x = 3047, .y = 495 }, .{ .x = 3057, .y = 479 }, .{ .x = 3069, .y = 489 }, .{ .x = 3076, .y = 524 }, .{ .x = 3098, .y = 546 }, .{ .x = 3106, .y = 566 }, .{ .x = 3119, .y = 568 }, .{ .x = 3112, .y = 531 }, .{ .x = 3118, .y = 522 }, .{ .x = 3129, .y = 527 }, .{ .x = 3137, .y = 543 }, .{ .x = 3160, .y = 553 }, .{ .x = 3179, .y = 575 }, .{ .x = 3187, .y = 550 }, .{ .x = 3219, .y = 548 }, .{ .x = 3239, .y = 539 }, .{ .x = 3263, .y = 514 }, .{ .x = 3265, .y = 489 }, .{ .x = 3256, .y = 470 }, .{ .x = 3261, .y = 468 }, .{ .x = 3304, .y = 493 }, .{ .x = 3346, .y = 500 }, .{ .x = 3545, .y = 621 }, .{ .x = 3598, .y = 640 }, .{ .x = 3623, .y = 637 }, .{ .x = 3606, .y = 651 }, .{ .x = 3609, .y = 723 }, .{ .x = 3622, .y = 781 }, .{ .x = 3643, .y = 806 }, .{ .x = 3679, .y = 794 }, .{ .x = 3682, .y = 810 }, .{ .x = 3637, .y = 907 }, .{ .x = 3608, .y = 1003 }, .{ .x = 3599, .y = 1004 }, .{ .x = 3571, .y = 978 }, .{ .x = 3508, .y = 966 }, .{ .x = 3491, .y = 942 }, .{ .x = 3487, .y = 917 }, .{ .x = 3480, .y = 910 }, .{ .x = 3487, .y = 966 }, .{ .x = 3504, .y = 1008 }, .{ .x = 3429, .y = 988 }, .{ .x = 3427, .y = 967 }, .{ .x = 3454, .y = 941 }, .{ .x = 3462, .y = 922 }, .{ .x = 3452, .y = 904 }, .{ .x = 3391, .y = 849 }, .{ .x = 3387, .y = 852 }, .{ .x = 3394, .y = 913 }, .{ .x = 3381, .y = 940 }, .{ .x = 3354, .y = 969 }, .{ .x = 3345, .y = 992 }, .{ .x = 3336, .y = 1062 }, .{ .x = 3268, .y = 1062 }, .{ .x = 3242, .y = 1050 }, .{ .x = 3217, .y = 1016 }, .{ .x = 3207, .y = 1011 }, .{ .x = 3204, .y = 1017 }, .{ .x = 3215, .y = 1032 }, .{ .x = 3216, .y = 1047 }, .{ .x = 3211, .y = 1075 }, .{ .x = 3220, .y = 1087 }, .{ .x = 3206, .y = 1127 }, .{ .x = 3196, .y = 1131 }, .{ .x = 3164, .y = 1119 }, .{ .x = 3128, .y = 1131 }, .{ .x = 3155, .y = 1141 }, .{ .x = 3162, .y = 1150 }, .{ .x = 3167, .y = 1181 }, .{ .x = 3160, .y = 1207 }, .{ .x = 3134, .y = 1214 }, .{ .x = 3080, .y = 1199 }, .{ .x = 3084, .y = 1208 }, .{ .x = 3130, .y = 1228 }, .{ .x = 3139, .y = 1240 }, .{ .x = 3151, .y = 1242 }, .{ .x = 3187, .y = 1212 }, .{ .x = 3193, .y = 1212 }, .{ .x = 3194, .y = 1230 }, .{ .x = 3213, .y = 1225 }, .{ .x = 3231, .y = 1232 }, .{ .x = 3234, .y = 1218 }, .{ .x = 3218, .y = 1208 }, .{ .x = 3220, .y = 1200 }, .{ .x = 3287, .y = 1210 }, .{ .x = 3274, .y = 1181 }, .{ .x = 3313, .y = 1198 }, .{ .x = 3307, .y = 1182 }, .{ .x = 3313, .y = 1173 }, .{ .x = 3386, .y = 1178 }, .{ .x = 3418, .y = 1169 }, .{ .x = 3440, .y = 1149 }, .{ .x = 3470, .y = 1148 }, .{ .x = 3494, .y = 1132 }, .{ .x = 3519, .y = 1129 }, .{ .x = 3536, .y = 1109 }, .{ .x = 3574, .y = 1101 }, .{ .x = 3586, .y = 1084 }, .{ .x = 3635, .y = 1075 }, .{ .x = 3642, .y = 1042 }, .{ .x = 3689, .y = 1059 }, .{ .x = 3717, .y = 1059 }, .{ .x = 3755, .y = 1046 }, .{ .x = 3776, .y = 1059 }, .{ .x = 3775, .y = 1075 }, .{ .x = 3768, .y = 1079 }, .{ .x = 3740, .y = 1080 }, .{ .x = 3701, .y = 1105 }, .{ .x = 3609, .y = 1131 }, .{ .x = 3359, .y = 1232 }, .{ .x = 2947, .y = 1386 }, .{ .x = 2831, .y = 1386 }, .{ .x = 2831, .y = 1371 }, .{ .x = 2834, .y = 1367 }, .{ .x = 2853, .y = 1377 }, .{ .x = 2877, .y = 1372 }, .{ .x = 2911, .y = 1353 }, .{ .x = 2951, .y = 1343 }, .{ .x = 2997, .y = 1313 }, .{ .x = 2987, .y = 1295 }, .{ .x = 2971, .y = 1288 }, .{ .x = 2955, .y = 1292 }, .{ .x = 2937, .y = 1282 }, .{ .x = 2932, .y = 1272 }, .{ .x = 2936, .y = 1228 }, .{ .x = 2950, .y = 1193 }, .{ .x = 2948, .y = 1142 }, .{ .x = 2943, .y = 1146 }, .{ .x = 2940, .y = 1185 }, .{ .x = 2920, .y = 1201 }, .{ .x = 2904, .y = 1244 }, .{ .x = 2904, .y = 1281 }, .{ .x = 2886, .y = 1283 }, .{ .x = 2870, .y = 1277 }, .{ .x = 2844, .y = 1232 }, .{ .x = 2845, .y = 1264 }, .{ .x = 2852, .y = 1282 }, .{ .x = 2895, .y = 1310 }, .{ .x = 2899, .y = 1333 }, .{ .x = 2870, .y = 1360 }, .{ .x = 2851, .y = 1360 }, .{ .x = 2842, .y = 1343 }, .{ .x = 2814, .y = 1352 }, .{ .x = 2796, .y = 1386 }, .{ .x = 2736, .y = 1381 }, .{ .x = 2711, .y = 1359 }, .{ .x = 2695, .y = 1353 }, .{ .x = 2691, .y = 1333 }, .{ .x = 2672, .y = 1310 }, .{ .x = 2659, .y = 1314 }, .{ .x = 2653, .y = 1338 }, .{ .x = 2628, .y = 1343 }, .{ .x = 2618, .y = 1353 }, .{ .x = 2592, .y = 1353 }, .{ .x = 2569, .y = 1342 }, .{ .x = 2542, .y = 1346 }, .{ .x = 2527, .y = 1256 }, .{ .x = 2522, .y = 1260 }, .{ .x = 2520, .y = 1337 }, .{ .x = 2512, .y = 1379 }, .{ .x = 2438, .y = 1386 }, .{ .x = 2408, .y = 1378 }, .{ .x = 2372, .y = 1359 }, .{ .x = 2326, .y = 1292 }, .{ .x = 2333, .y = 1245 }, .{ .x = 2373, .y = 1220 }, .{ .x = 2347, .y = 1223 }, .{ .x = 2300, .y = 1261 }, .{ .x = 2299, .y = 1203 }, .{ .x = 2291, .y = 1211 }, .{ .x = 2279, .y = 1249 }, .{ .x = 2250, .y = 1224 }, .{ .x = 2246, .y = 1227 }, .{ .x = 2257, .y = 1251 }, .{ .x = 2289, .y = 1280 }, .{ .x = 2279, .y = 1312 }, .{ .x = 2281, .y = 1366 }, .{ .x = 2273, .y = 1373 }, .{ .x = 2229, .y = 1374 }, .{ .x = 2203, .y = 1352 }, .{ .x = 2168, .y = 1353 }, .{ .x = 2138, .y = 1325 }, .{ .x = 2134, .y = 1332 }, .{ .x = 2147, .y = 1359 }, .{ .x = 2151, .y = 1386 }, .{ .x = 1712, .y = 1386 }, .{ .x = 1709, .y = 1358 }, .{ .x = 1701, .y = 1362 }, .{ .x = 1700, .y = 1386 }, .{ .x = -64, .y = 1386 }, .{ .x = -64, .y = 128 }, .{ .x = -52, .y = 110 }, .{ .x = -53, .y = 101 }, .{ .x = -64, .y = 102 }, .{ .x = -64, .y = 90 }, .{ .x = -39, .y = 79 }, .{ .x = 3, .y = 45 }, .{ .x = 34, .y = 45 }, .{ .x = 41, .y = 65 }, .{ .x = 34, .y = 93 }, .{ .x = 26, .y = 107 }, .{ .x = -8, .y = 133 }, .{ .x = -20, .y = 156 }, .{ .x = -7, .y = 197 }, .{ .x = 27, .y = 200 }, .{ .x = 43, .y = 232 }, .{ .x = 53, .y = 176 }, .{ .x = 44, .y = 110 }, .{ .x = 65, .y = 68 }, .{ .x = 88, .y = 82 }, .{ .x = 98, .y = 97 }, .{ .x = 105, .y = 129 }, .{ .x = 131, .y = 138 }, .{ .x = 142, .y = 152 }, .{ .x = 134, .y = 174 }, .{ .x = 134, .y = 205 }, .{ .x = 113, .y = 212 }, .{ .x = 74, .y = 202 }, .{ .x = 68, .y = 228 }, .{ .x = 100, .y = 236 }, .{ .x = 118, .y = 266 }, .{ .x = 126, .y = 268 }, .{ .x = 135, .y = 267 }, .{ .x = 141, .y = 232 }, .{ .x = 176, .y = 222 }, .{ .x = 176, .y = 213 }, .{ .x = 168, .y = 208 }, .{ .x = 162, .y = 169 }, .{ .x = 170, .y = 160 }, .{ .x = 184, .y = 171 }, .{ .x = 218, .y = 179 }, .{ .x = 242, .y = 204 }, .{ .x = 260, .y = 206 }, .{ .x = 302, .y = 250 }, .{ .x = 324, .y = 246 }, .{ .x = 332, .y = 234 }, .{ .x = 340, .y = 204 }, .{ .x = 313, .y = 177 }, .{ .x = 272, .y = 97 }, .{ .x = 272, .y = 70 }, .{ .x = 251, .y = 62 }, .{ .x = 241, .y = 77 }, .{ .x = 225, .y = 66 }, .{ .x = 205, .y = 78 }, .{ .x = 198, .y = 63 }, .{ .x = 198, .y = 58 }, .{ .x = 209, .y = 61 }, .{ .x = 231, .y = 47 }, .{ .x = 248, .y = 44 }, .{ .x = 285, .y = 53 }, .{ .x = 369, .y = 99 }, .{ .x = 435, .y = 115 }, .{ .x = 463, .y = 128 }, .{ .x = 491, .y = 160 }, .{ .x = 506, .y = 168 }, .{ .x = 509, .y = 184 }, .{ .x = 537, .y = 210 }, .{ .x = 550, .y = 197 }, .{ .x = 573, .y = 191 }, .{ .x = 591, .y = 156 }, .{ .x = 526, .y = 160 }, .{ .x = 515, .y = 143 }, .{ .x = 587, .y = 140 }, .{ .x = 594, .y = 136 }, .{ .x = 595, .y = 124 }, .{ .x = 488, .y = 123 }, .{ .x = 479, .y = 120 }, .{ .x = 480, .y = 108 }, .{ .x = 726, .y = 104 }, .{ .x = 794, .y = 109 }, .{ .x = 886, .y = 98 }, .{ .x = 1001, .y = 103 }, .{ .x = 1107, .y = 126 }, .{ .x = 1330, .y = 143 }, .{ .x = 1448, .y = 130 }, .{ .x = 1496, .y = 117 }, .{ .x = 1515, .y = 117 }, .{ .x = 1519, .y = 135 }, .{ .x = 1531, .y = 137 }, .{ .x = 1547, .y = 130 }, .{ .x = 1530, .y = 125 }, .{ .x = 1531, .y = 113 }, .{ .x = 1538, .y = 109 }, .{ .x = 1604, .y = 92 }, .{ .x = 1721, .y = 77 }, .{ .x = 1849, .y = 90 }, .{ .x = 1885, .y = 103 }, .{ .x = 1944, .y = 100 }, .{ .x = 2019, .y = 113 }, .{ .x = 2107, .y = 104 }, .{ .x = 2332, .y = 51 }, .{ .x = 2486, .y = -4 }, .{ .x = 2566, .y = -7 }, .{ .x = 2658, .y = 5 }, .{ .x = 2732, .y = -6 }, .{ .x = 2836, .y = -11 }, .{ .x = 2881, .y = -24 }, .{ .x = 2958, .y = -64 } }; +const LI_F0 = [_]Pt{ .{ .x = -64, .y = 1386 }, .{ .x = -64, .y = -64 }, .{ .x = 4160, .y = -64 }, .{ .x = 4160, .y = 1386 }, .{ .x = 4038, .y = 1386 }, .{ .x = 4032, .y = 1386 }, .{ .x = 3457, .y = 1386 }, .{ .x = 2947, .y = 1386 }, .{ .x = 2831, .y = 1386 }, .{ .x = 2788, .y = 1386 }, .{ .x = 2755, .y = 1386 }, .{ .x = 2151, .y = 1386 }, .{ .x = 2108, .y = 1386 }, .{ .x = 2086, .y = 1386 }, .{ .x = 1991, .y = 1386 }, .{ .x = 1959, .y = 1386 }, .{ .x = 1712, .y = 1386 }, .{ .x = 1700, .y = 1386 }, .{ .x = 1677, .y = 1386 }, .{ .x = 1597, .y = 1386 } }; +const LI_SUBJECT = [_][]const Pt{&LI_S0}; +const LI_FACE = [_][]const Pt{&LI_F0}; + +test "compose clip repro: Long Island land ring vs coverage-edge face" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const r = try boolean.compute(a, &LI_SUBJECT, &LI_FACE, .intersect); + const probes = [_]Pt{ .{ .x = 3320, .y = 100 }, .{ .x = 3380, .y = 100 }, .{ .x = 3320, .y = 160 }, .{ .x = 3320, .y = 220 }, .{ .x = 3320, .y = 520 }, .{ .x = 3320, .y = 580 } }; + for (probes) |q| { + try std.testing.expect(boolean.pointInEvenOdd(&LI_SUBJECT, q.x, q.y)); + try std.testing.expect(boolean.pointInEvenOdd(&LI_FACE, q.x, q.y)); + if (!boolean.pointInEvenOdd(r, q.x, q.y)) { + std.debug.print("land point ({d},{d}) missing from intersection\n", .{ q.x, q.y }); + return error.LandExcluded; + } + } +} + +const SM_S0 = [_]Pt{ .{ .x = 2075, .y = 1078 }, .{ .x = 2075, .y = 1205 }, .{ .x = 1966, .y = 1205 }, .{ .x = 1966, .y = 1066 }, .{ .x = 1981, .y = 1076 }, .{ .x = 2002, .y = 1067 }, .{ .x = 2017, .y = 1081 }, .{ .x = 2039, .y = 1066 }, .{ .x = 2058, .y = 1066 }, .{ .x = 2075, .y = 1078 } }; +const SM_F0 = [_]Pt{ .{ .x = 1966, .y = 1205 }, .{ .x = 1966, .y = 1066 }, .{ .x = 1967, .y = 1066 }, .{ .x = 1968, .y = 1066 }, .{ .x = 1969, .y = 1066 }, .{ .x = 1970, .y = 1066 }, .{ .x = 1970, .y = 1066 }, .{ .x = 1970, .y = 1066 }, .{ .x = 1971, .y = 1066 }, .{ .x = 1974, .y = 1066 }, .{ .x = 1994, .y = 1066 }, .{ .x = 1994, .y = 1066 }, .{ .x = 1994, .y = 1066 }, .{ .x = 1996, .y = 1066 }, .{ .x = 2001, .y = 1066 }, .{ .x = 2002, .y = 1066 }, .{ .x = 2005, .y = 1066 }, .{ .x = 2015, .y = 1066 }, .{ .x = 2015, .y = 1076 }, .{ .x = 2015, .y = 1079 }, .{ .x = 2015, .y = 1205 } }; +const SM_SUBJECT = [_][]const Pt{&SM_S0}; +const SM_FACE = [_][]const Pt{&SM_F0}; + +test "compose clip repro: micro-subdivided seam edge with duplicate vertices" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const before = boolean.open_chain_walks; + const r = try boolean.compute(a, &SM_SUBJECT, &SM_FACE, .intersect); + try std.testing.expectEqual(before, boolean.open_chain_walks); // every walk must close + // Interior of subject ∩ face must be present. + const probes = [_]Pt{ .{ .x = 1990, .y = 1100 }, .{ .x = 2005, .y = 1150 }, .{ .x = 1975, .y = 1190 } }; + for (probes) |q| { + try std.testing.expect(boolean.pointInEvenOdd(&SM_SUBJECT, q.x, q.y)); + try std.testing.expect(boolean.pointInEvenOdd(&SM_FACE, q.x, q.y)); + try std.testing.expect(boolean.pointInEvenOdd(r, q.x, q.y)); + } +} + +const C2_S0 = [_]Pt{ .{ .x = 3088, .y = 2392 }, .{ .x = 3077, .y = 2398 }, .{ .x = 3090, .y = 2394 }, .{ .x = 3097, .y = 2402 }, .{ .x = 3083, .y = 2400 }, .{ .x = 3087, .y = 2404 }, .{ .x = 3079, .y = 2411 }, .{ .x = 3071, .y = 2406 }, .{ .x = 3063, .y = 2420 }, .{ .x = 3055, .y = 2421 }, .{ .x = 3045, .y = 2435 }, .{ .x = 3025, .y = 2440 }, .{ .x = 3012, .y = 2456 }, .{ .x = 2978, .y = 2459 }, .{ .x = 2983, .y = 2464 }, .{ .x = 2969, .y = 2466 }, .{ .x = 2972, .y = 2476 }, .{ .x = 2978, .y = 2476 }, .{ .x = 2973, .y = 2478 }, .{ .x = 2974, .y = 2501 }, .{ .x = 2982, .y = 2511 }, .{ .x = 2993, .y = 2509 }, .{ .x = 3001, .y = 2517 }, .{ .x = 2990, .y = 2528 }, .{ .x = 2983, .y = 2524 }, .{ .x = 2970, .y = 2541 }, .{ .x = 2982, .y = 2523 }, .{ .x = 2999, .y = 2516 }, .{ .x = 2979, .y = 2516 }, .{ .x = 2968, .y = 2500 }, .{ .x = 2959, .y = 2506 }, .{ .x = 2959, .y = 2490 }, .{ .x = 2949, .y = 2492 }, .{ .x = 2955, .y = 2459 }, .{ .x = 2932, .y = 2458 }, .{ .x = 2933, .y = 2446 }, .{ .x = 2917, .y = 2457 }, .{ .x = 2917, .y = 2462 }, .{ .x = 2928, .y = 2465 }, .{ .x = 2916, .y = 2474 }, .{ .x = 2918, .y = 2488 }, .{ .x = 2912, .y = 2496 }, .{ .x = 2904, .y = 2472 }, .{ .x = 2894, .y = 2470 }, .{ .x = 2887, .y = 2485 }, .{ .x = 2876, .y = 2483 }, .{ .x = 2876, .y = 2474 }, .{ .x = 2871, .y = 2478 }, .{ .x = 2871, .y = 2500 }, .{ .x = 2857, .y = 2505 }, .{ .x = 2857, .y = 2515 }, .{ .x = 2845, .y = 2528 }, .{ .x = 2854, .y = 2533 }, .{ .x = 2857, .y = 2550 }, .{ .x = 2845, .y = 2555 }, .{ .x = 2844, .y = 2579 }, .{ .x = 2833, .y = 2580 }, .{ .x = 2827, .y = 2597 }, .{ .x = 2814, .y = 2591 }, .{ .x = 2803, .y = 2604 }, .{ .x = 2805, .y = 2618 }, .{ .x = 2813, .y = 2614 }, .{ .x = 2822, .y = 2622 }, .{ .x = 2810, .y = 2643 }, .{ .x = 2810, .y = 2658 }, .{ .x = 2791, .y = 2674 }, .{ .x = 2807, .y = 2680 }, .{ .x = 2817, .y = 2723 }, .{ .x = 2826, .y = 2718 }, .{ .x = 2841, .y = 2722 }, .{ .x = 2839, .y = 2737 }, .{ .x = 2830, .y = 2745 }, .{ .x = 2833, .y = 2755 }, .{ .x = 2853, .y = 2738 }, .{ .x = 2881, .y = 2731 }, .{ .x = 2837, .y = 2763 }, .{ .x = 2839, .y = 2772 }, .{ .x = 2858, .y = 2767 }, .{ .x = 2879, .y = 2770 }, .{ .x = 2884, .y = 2783 }, .{ .x = 2874, .y = 2787 }, .{ .x = 2878, .y = 2800 }, .{ .x = 2887, .y = 2802 }, .{ .x = 2878, .y = 2809 }, .{ .x = 2889, .y = 2812 }, .{ .x = 2892, .y = 2795 }, .{ .x = 2907, .y = 2799 }, .{ .x = 2914, .y = 2782 }, .{ .x = 2902, .y = 2786 }, .{ .x = 2892, .y = 2779 }, .{ .x = 2900, .y = 2753 }, .{ .x = 2909, .y = 2747 }, .{ .x = 2917, .y = 2751 }, .{ .x = 2917, .y = 2741 }, .{ .x = 2924, .y = 2736 }, .{ .x = 2941, .y = 2735 }, .{ .x = 2956, .y = 2711 }, .{ .x = 2953, .y = 2701 }, .{ .x = 2960, .y = 2710 }, .{ .x = 2974, .y = 2698 }, .{ .x = 2956, .y = 2712 }, .{ .x = 2965, .y = 2725 }, .{ .x = 2969, .y = 2755 }, .{ .x = 2957, .y = 2760 }, .{ .x = 2950, .y = 2773 }, .{ .x = 2958, .y = 2786 }, .{ .x = 2974, .y = 2792 }, .{ .x = 2984, .y = 2761 }, .{ .x = 3019, .y = 2767 }, .{ .x = 3046, .y = 2726 }, .{ .x = 3055, .y = 2731 }, .{ .x = 3062, .y = 2726 }, .{ .x = 3067, .y = 2709 }, .{ .x = 3098, .y = 2708 }, .{ .x = 3094, .y = 2697 }, .{ .x = 3105, .y = 2693 }, .{ .x = 3107, .y = 2685 }, .{ .x = 3091, .y = 2670 }, .{ .x = 3093, .y = 2659 }, .{ .x = 3082, .y = 2660 }, .{ .x = 3098, .y = 2641 }, .{ .x = 3096, .y = 2634 }, .{ .x = 3088, .y = 2637 }, .{ .x = 3099, .y = 2631 }, .{ .x = 3096, .y = 2625 }, .{ .x = 3101, .y = 2626 }, .{ .x = 3104, .y = 2649 }, .{ .x = 3134, .y = 2635 }, .{ .x = 3138, .y = 2627 }, .{ .x = 3155, .y = 2639 }, .{ .x = 3173, .y = 2625 }, .{ .x = 3174, .y = 2641 }, .{ .x = 3177, .y = 2633 }, .{ .x = 3204, .y = 2616 }, .{ .x = 3205, .y = 2608 }, .{ .x = 3197, .y = 2607 }, .{ .x = 3211, .y = 2599 }, .{ .x = 3218, .y = 2600 }, .{ .x = 3216, .y = 2607 }, .{ .x = 3225, .y = 2605 }, .{ .x = 3263, .y = 2567 }, .{ .x = 3298, .y = 2602 }, .{ .x = 3296, .y = 2614 }, .{ .x = 3306, .y = 2619 }, .{ .x = 3305, .y = 2626 }, .{ .x = 3318, .y = 2622 }, .{ .x = 3404, .y = 2707 }, .{ .x = 3369, .y = 2726 }, .{ .x = 3341, .y = 2757 }, .{ .x = 3372, .y = 2757 }, .{ .x = 3421, .y = 2726 }, .{ .x = 3443, .y = 2748 }, .{ .x = 3424, .y = 2760 }, .{ .x = 3423, .y = 2770 }, .{ .x = 3408, .y = 2773 }, .{ .x = 3399, .y = 2792 }, .{ .x = 3407, .y = 2768 }, .{ .x = 3380, .y = 2782 }, .{ .x = 3383, .y = 2786 }, .{ .x = 3375, .y = 2782 }, .{ .x = 3372, .y = 2790 }, .{ .x = 3362, .y = 2788 }, .{ .x = 3367, .y = 2804 }, .{ .x = 3360, .y = 2823 }, .{ .x = 3348, .y = 2831 }, .{ .x = 3336, .y = 2829 }, .{ .x = 3323, .y = 2844 }, .{ .x = 3324, .y = 2855 }, .{ .x = 3319, .y = 2856 }, .{ .x = 3315, .y = 2839 }, .{ .x = 3313, .y = 2861 }, .{ .x = 3312, .y = 2839 }, .{ .x = 3323, .y = 2830 }, .{ .x = 3311, .y = 2825 }, .{ .x = 3272, .y = 2867 }, .{ .x = 3272, .y = 2829 }, .{ .x = 3264, .y = 2845 }, .{ .x = 3243, .y = 2826 }, .{ .x = 3207, .y = 2842 }, .{ .x = 3204, .y = 2854 }, .{ .x = 3218, .y = 2866 }, .{ .x = 3206, .y = 2870 }, .{ .x = 3216, .y = 2908 }, .{ .x = 3212, .y = 2920 }, .{ .x = 3230, .y = 2909 }, .{ .x = 3219, .y = 2934 }, .{ .x = 3222, .y = 2943 }, .{ .x = 3233, .y = 2943 }, .{ .x = 3221, .y = 2947 }, .{ .x = 3215, .y = 2926 }, .{ .x = 3215, .y = 2949 }, .{ .x = 3211, .y = 2947 }, .{ .x = 3222, .y = 2954 }, .{ .x = 3209, .y = 2947 }, .{ .x = 3217, .y = 2955 }, .{ .x = 3204, .y = 2957 }, .{ .x = 3192, .y = 2987 }, .{ .x = 3184, .y = 2987 }, .{ .x = 3172, .y = 3002 }, .{ .x = 3147, .y = 3008 }, .{ .x = 3147, .y = 3020 }, .{ .x = 3134, .y = 3020 }, .{ .x = 3143, .y = 3017 }, .{ .x = 3146, .y = 3006 }, .{ .x = 3172, .y = 2997 }, .{ .x = 3172, .y = 2984 }, .{ .x = 3155, .y = 2973 }, .{ .x = 3164, .y = 2976 }, .{ .x = 3190, .y = 2959 }, .{ .x = 3198, .y = 2960 }, .{ .x = 3203, .y = 2944 }, .{ .x = 3193, .y = 2945 }, .{ .x = 3202, .y = 2945 }, .{ .x = 3214, .y = 2922 }, .{ .x = 3210, .y = 2917 }, .{ .x = 3175, .y = 2917 }, .{ .x = 3177, .y = 2909 }, .{ .x = 3163, .y = 2907 }, .{ .x = 3154, .y = 2897 }, .{ .x = 3132, .y = 2916 }, .{ .x = 3129, .y = 2912 }, .{ .x = 3144, .y = 2902 }, .{ .x = 3130, .y = 2906 }, .{ .x = 3130, .y = 2911 }, .{ .x = 3128, .y = 2905 }, .{ .x = 3149, .y = 2897 }, .{ .x = 3149, .y = 2889 }, .{ .x = 3125, .y = 2881 }, .{ .x = 3115, .y = 2886 }, .{ .x = 3121, .y = 2879 }, .{ .x = 3103, .y = 2864 }, .{ .x = 3091, .y = 2877 }, .{ .x = 3090, .y = 2893 }, .{ .x = 3079, .y = 2895 }, .{ .x = 3065, .y = 2941 }, .{ .x = 3032, .y = 2970 }, .{ .x = 2977, .y = 2997 }, .{ .x = 2946, .y = 3001 }, .{ .x = 2937, .y = 2988 }, .{ .x = 2900, .y = 3005 }, .{ .x = 2891, .y = 3015 }, .{ .x = 2858, .y = 3026 }, .{ .x = 2854, .y = 3017 }, .{ .x = 2843, .y = 3016 }, .{ .x = 2827, .y = 3025 }, .{ .x = 2799, .y = 3028 }, .{ .x = 2741, .y = 3071 }, .{ .x = 2648, .y = 2978 }, .{ .x = 2775, .y = 2860 }, .{ .x = 2789, .y = 2527 }, .{ .x = 2968, .y = 2400 }, .{ .x = 2978, .y = 2410 }, .{ .x = 3043, .y = 2411 }, .{ .x = 3080, .y = 2386 }, .{ .x = 3088, .y = 2392 } }; +const C2_S1 = [_]Pt{ .{ .x = 2972, .y = 2408 }, .{ .x = 2971, .y = 2408 }, .{ .x = 2971, .y = 2409 }, .{ .x = 2969, .y = 2409 }, .{ .x = 2969, .y = 2410 }, .{ .x = 2968, .y = 2411 }, .{ .x = 2968, .y = 2412 }, .{ .x = 2967, .y = 2413 }, .{ .x = 2967, .y = 2416 }, .{ .x = 2973, .y = 2410 }, .{ .x = 2973, .y = 2409 }, .{ .x = 2972, .y = 2409 }, .{ .x = 2972, .y = 2408 } }; +const C2_S2 = [_]Pt{ .{ .x = 2973, .y = 2414 }, .{ .x = 2974, .y = 2414 }, .{ .x = 2974, .y = 2413 }, .{ .x = 2973, .y = 2413 }, .{ .x = 2973, .y = 2414 } }; +const C2_S3 = [_]Pt{ .{ .x = 3054, .y = 2416 }, .{ .x = 3052, .y = 2416 }, .{ .x = 3051, .y = 2417 }, .{ .x = 3051, .y = 2420 }, .{ .x = 3052, .y = 2420 }, .{ .x = 3054, .y = 2418 }, .{ .x = 3054, .y = 2417 }, .{ .x = 3056, .y = 2417 }, .{ .x = 3056, .y = 2415 }, .{ .x = 3055, .y = 2415 }, .{ .x = 3054, .y = 2416 } }; +const C2_S4 = [_]Pt{ .{ .x = 3022, .y = 2420 }, .{ .x = 3021, .y = 2420 }, .{ .x = 3021, .y = 2421 }, .{ .x = 3022, .y = 2421 }, .{ .x = 3022, .y = 2420 } }; +const C2_S5 = [_]Pt{ .{ .x = 2950, .y = 2425 }, .{ .x = 2955, .y = 2421 }, .{ .x = 2950, .y = 2420 }, .{ .x = 2939, .y = 2429 }, .{ .x = 2942, .y = 2435 }, .{ .x = 2933, .y = 2439 }, .{ .x = 2935, .y = 2443 }, .{ .x = 2962, .y = 2426 }, .{ .x = 2960, .y = 2422 }, .{ .x = 2947, .y = 2431 }, .{ .x = 2950, .y = 2425 } }; +const C2_S6 = [_]Pt{ .{ .x = 3040, .y = 2433 }, .{ .x = 3041, .y = 2433 }, .{ .x = 3041, .y = 2432 }, .{ .x = 3040, .y = 2432 }, .{ .x = 3040, .y = 2433 } }; +const C2_S7 = [_]Pt{ .{ .x = 2975, .y = 2435 }, .{ .x = 2974, .y = 2435 }, .{ .x = 2974, .y = 2434 }, .{ .x = 2973, .y = 2434 }, .{ .x = 2972, .y = 2435 }, .{ .x = 2971, .y = 2435 }, .{ .x = 2971, .y = 2438 }, .{ .x = 2972, .y = 2438 }, .{ .x = 2975, .y = 2435 } }; +const C2_S8 = [_]Pt{ .{ .x = 2966, .y = 2445 }, .{ .x = 2972, .y = 2440 }, .{ .x = 2964, .y = 2440 }, .{ .x = 2966, .y = 2445 } }; +const C2_S9 = [_]Pt{ .{ .x = 2950, .y = 2440 }, .{ .x = 2950, .y = 2441 }, .{ .x = 2951, .y = 2441 }, .{ .x = 2950, .y = 2440 } }; +const C2_S10 = [_]Pt{ .{ .x = 2931, .y = 2439 }, .{ .x = 2931, .y = 2438 }, .{ .x = 2932, .y = 2438 }, .{ .x = 2932, .y = 2437 }, .{ .x = 2930, .y = 2437 }, .{ .x = 2930, .y = 2436 }, .{ .x = 2928, .y = 2436 }, .{ .x = 2928, .y = 2437 }, .{ .x = 2927, .y = 2437 }, .{ .x = 2926, .y = 2438 }, .{ .x = 2926, .y = 2439 }, .{ .x = 2925, .y = 2440 }, .{ .x = 2925, .y = 2441 }, .{ .x = 2929, .y = 2441 }, .{ .x = 2929, .y = 2440 }, .{ .x = 2930, .y = 2440 }, .{ .x = 2930, .y = 2439 }, .{ .x = 2931, .y = 2439 } }; +const C2_S11 = [_]Pt{ .{ .x = 3020, .y = 2416 }, .{ .x = 3019, .y = 2416 }, .{ .x = 3019, .y = 2417 }, .{ .x = 3018, .y = 2417 }, .{ .x = 3018, .y = 2418 }, .{ .x = 3019, .y = 2418 }, .{ .x = 3019, .y = 2417 }, .{ .x = 3020, .y = 2416 } }; +const C2_S12 = [_]Pt{ .{ .x = 2863, .y = 2485 }, .{ .x = 2864, .y = 2485 }, .{ .x = 2864, .y = 2484 }, .{ .x = 2863, .y = 2484 }, .{ .x = 2863, .y = 2485 } }; +const C2_S13 = [_]Pt{ .{ .x = 2853, .y = 2496 }, .{ .x = 2854, .y = 2495 }, .{ .x = 2849, .y = 2495 }, .{ .x = 2849, .y = 2497 }, .{ .x = 2847, .y = 2499 }, .{ .x = 2847, .y = 2502 }, .{ .x = 2849, .y = 2502 }, .{ .x = 2850, .y = 2500 }, .{ .x = 2852, .y = 2500 }, .{ .x = 2852, .y = 2499 }, .{ .x = 2853, .y = 2499 }, .{ .x = 2853, .y = 2498 }, .{ .x = 2854, .y = 2498 }, .{ .x = 2854, .y = 2497 }, .{ .x = 2853, .y = 2497 }, .{ .x = 2853, .y = 2496 } }; +const C2_S14 = [_]Pt{ .{ .x = 2854, .y = 2500 }, .{ .x = 2854, .y = 2499 }, .{ .x = 2853, .y = 2499 }, .{ .x = 2853, .y = 2500 }, .{ .x = 2852, .y = 2500 }, .{ .x = 2852, .y = 2501 }, .{ .x = 2853, .y = 2501 }, .{ .x = 2853, .y = 2500 }, .{ .x = 2854, .y = 2500 } }; +const C2_S15 = [_]Pt{ .{ .x = 2834, .y = 2527 }, .{ .x = 2835, .y = 2527 }, .{ .x = 2835, .y = 2524 }, .{ .x = 2834, .y = 2524 }, .{ .x = 2833, .y = 2525 }, .{ .x = 2833, .y = 2526 }, .{ .x = 2834, .y = 2527 } }; +const C2_S16 = [_]Pt{ .{ .x = 2837, .y = 2527 }, .{ .x = 2837, .y = 2528 }, .{ .x = 2836, .y = 2528 }, .{ .x = 2837, .y = 2529 }, .{ .x = 2837, .y = 2531 }, .{ .x = 2839, .y = 2531 }, .{ .x = 2840, .y = 2530 }, .{ .x = 2840, .y = 2527 }, .{ .x = 2841, .y = 2527 }, .{ .x = 2840, .y = 2526 }, .{ .x = 2839, .y = 2526 }, .{ .x = 2839, .y = 2525 }, .{ .x = 2838, .y = 2525 }, .{ .x = 2838, .y = 2526 }, .{ .x = 2837, .y = 2527 } }; +const C2_S17 = [_]Pt{ .{ .x = 2813, .y = 2530 }, .{ .x = 2815, .y = 2528 }, .{ .x = 2812, .y = 2528 }, .{ .x = 2812, .y = 2530 }, .{ .x = 2813, .y = 2530 } }; +const C2_S18 = [_]Pt{ .{ .x = 2838, .y = 2574 }, .{ .x = 2838, .y = 2573 }, .{ .x = 2837, .y = 2574 }, .{ .x = 2838, .y = 2574 } }; +const C2_S19 = [_]Pt{ .{ .x = 2828, .y = 2575 }, .{ .x = 2827, .y = 2575 }, .{ .x = 2827, .y = 2576 }, .{ .x = 2828, .y = 2577 }, .{ .x = 2830, .y = 2575 }, .{ .x = 2828, .y = 2575 } }; +const C2_S20 = [_]Pt{ .{ .x = 2812, .y = 2586 }, .{ .x = 2812, .y = 2585 }, .{ .x = 2811, .y = 2585 }, .{ .x = 2811, .y = 2586 }, .{ .x = 2812, .y = 2586 } }; +const C2_S21 = [_]Pt{ .{ .x = 2828, .y = 2591 }, .{ .x = 2829, .y = 2591 }, .{ .x = 2829, .y = 2590 }, .{ .x = 2828, .y = 2590 }, .{ .x = 2828, .y = 2591 } }; +const C2_S22 = [_]Pt{ .{ .x = 2800, .y = 2596 }, .{ .x = 2801, .y = 2596 }, .{ .x = 2801, .y = 2595 }, .{ .x = 2802, .y = 2595 }, .{ .x = 2802, .y = 2596 }, .{ .x = 2803, .y = 2596 }, .{ .x = 2803, .y = 2595 }, .{ .x = 2804, .y = 2595 }, .{ .x = 2804, .y = 2594 }, .{ .x = 2800, .y = 2594 }, .{ .x = 2800, .y = 2595 }, .{ .x = 2799, .y = 2596 }, .{ .x = 2799, .y = 2597 }, .{ .x = 2800, .y = 2597 }, .{ .x = 2800, .y = 2596 } }; +const C2_S23 = [_]Pt{ .{ .x = 2796, .y = 2602 }, .{ .x = 2797, .y = 2602 }, .{ .x = 2797, .y = 2601 }, .{ .x = 2796, .y = 2601 }, .{ .x = 2796, .y = 2602 } }; +const C2_S24 = [_]Pt{ .{ .x = 2795, .y = 2604 }, .{ .x = 2796, .y = 2604 }, .{ .x = 2796, .y = 2602 }, .{ .x = 2795, .y = 2602 }, .{ .x = 2795, .y = 2604 } }; +const C2_S25 = [_]Pt{ .{ .x = 2796, .y = 2608 }, .{ .x = 2794, .y = 2608 }, .{ .x = 2794, .y = 2609 }, .{ .x = 2793, .y = 2609 }, .{ .x = 2793, .y = 2611 }, .{ .x = 2794, .y = 2611 }, .{ .x = 2794, .y = 2610 }, .{ .x = 2795, .y = 2610 }, .{ .x = 2795, .y = 2611 }, .{ .x = 2796, .y = 2610 }, .{ .x = 2797, .y = 2610 }, .{ .x = 2797, .y = 2607 }, .{ .x = 2795, .y = 2607 }, .{ .x = 2795, .y = 2608 }, .{ .x = 2796, .y = 2608 } }; +const C2_S26 = [_]Pt{ .{ .x = 2800, .y = 2610 }, .{ .x = 2801, .y = 2610 }, .{ .x = 2801, .y = 2609 }, .{ .x = 2802, .y = 2608 }, .{ .x = 2801, .y = 2608 }, .{ .x = 2800, .y = 2609 }, .{ .x = 2800, .y = 2610 } }; +const C2_S27 = [_]Pt{ .{ .x = 2803, .y = 2612 }, .{ .x = 2802, .y = 2612 }, .{ .x = 2802, .y = 2613 }, .{ .x = 2803, .y = 2613 }, .{ .x = 2803, .y = 2612 } }; +const C2_S28 = [_]Pt{ .{ .x = 2795, .y = 2612 }, .{ .x = 2794, .y = 2612 }, .{ .x = 2793, .y = 2613 }, .{ .x = 2793, .y = 2615 }, .{ .x = 2794, .y = 2615 }, .{ .x = 2795, .y = 2614 }, .{ .x = 2795, .y = 2612 } }; +const C2_S29 = [_]Pt{ .{ .x = 2801, .y = 2615 }, .{ .x = 2801, .y = 2616 }, .{ .x = 2800, .y = 2616 }, .{ .x = 2800, .y = 2617 }, .{ .x = 2802, .y = 2617 }, .{ .x = 2802, .y = 2616 }, .{ .x = 2801, .y = 2615 } }; +const C2_S30 = [_]Pt{ .{ .x = 2797, .y = 2616 }, .{ .x = 2796, .y = 2617 }, .{ .x = 2797, .y = 2617 }, .{ .x = 2797, .y = 2616 } }; +const C2_S31 = [_]Pt{ .{ .x = 2810, .y = 2619 }, .{ .x = 2809, .y = 2619 }, .{ .x = 2809, .y = 2620 }, .{ .x = 2808, .y = 2620 }, .{ .x = 2808, .y = 2621 }, .{ .x = 2809, .y = 2621 }, .{ .x = 2810, .y = 2621 }, .{ .x = 2810, .y = 2620 }, .{ .x = 2811, .y = 2619 }, .{ .x = 2810, .y = 2619 } }; +const C2_S32 = [_]Pt{ .{ .x = 2812, .y = 2635 }, .{ .x = 2811, .y = 2635 }, .{ .x = 2811, .y = 2636 }, .{ .x = 2812, .y = 2636 }, .{ .x = 2812, .y = 2635 } }; +const C2_S33 = [_]Pt{ .{ .x = 2808, .y = 2640 }, .{ .x = 2809, .y = 2640 }, .{ .x = 2809, .y = 2639 }, .{ .x = 2808, .y = 2639 }, .{ .x = 2808, .y = 2640 } }; +const C2_S34 = [_]Pt{ .{ .x = 2798, .y = 2651 }, .{ .x = 2797, .y = 2652 }, .{ .x = 2796, .y = 2652 }, .{ .x = 2796, .y = 2653 }, .{ .x = 2795, .y = 2654 }, .{ .x = 2794, .y = 2654 }, .{ .x = 2794, .y = 2656 }, .{ .x = 2795, .y = 2656 }, .{ .x = 2795, .y = 2657 }, .{ .x = 2796, .y = 2656 }, .{ .x = 2797, .y = 2656 }, .{ .x = 2797, .y = 2655 }, .{ .x = 2798, .y = 2654 }, .{ .x = 2799, .y = 2654 }, .{ .x = 2801, .y = 2652 }, .{ .x = 2801, .y = 2649 }, .{ .x = 2800, .y = 2650 }, .{ .x = 2798, .y = 2651 } }; +const C2_S35 = [_]Pt{ .{ .x = 2792, .y = 2651 }, .{ .x = 2791, .y = 2652 }, .{ .x = 2790, .y = 2652 }, .{ .x = 2790, .y = 2653 }, .{ .x = 2792, .y = 2653 }, .{ .x = 2792, .y = 2651 } }; +const C2_S36 = [_]Pt{ .{ .x = 2797, .y = 2710 }, .{ .x = 2798, .y = 2710 }, .{ .x = 2799, .y = 2709 }, .{ .x = 2800, .y = 2709 }, .{ .x = 2801, .y = 2707 }, .{ .x = 2802, .y = 2707 }, .{ .x = 2802, .y = 2706 }, .{ .x = 2801, .y = 2706 }, .{ .x = 2801, .y = 2707 }, .{ .x = 2800, .y = 2708 }, .{ .x = 2798, .y = 2708 }, .{ .x = 2798, .y = 2707 }, .{ .x = 2799, .y = 2706 }, .{ .x = 2797, .y = 2706 }, .{ .x = 2797, .y = 2707 }, .{ .x = 2796, .y = 2708 }, .{ .x = 2796, .y = 2710 }, .{ .x = 2797, .y = 2710 } }; +const C2_S37 = [_]Pt{ .{ .x = 2814, .y = 2724 }, .{ .x = 2815, .y = 2724 }, .{ .x = 2815, .y = 2722 }, .{ .x = 2814, .y = 2723 }, .{ .x = 2813, .y = 2723 }, .{ .x = 2813, .y = 2724 }, .{ .x = 2814, .y = 2724 } }; +const C2_S38 = [_]Pt{ .{ .x = 2839, .y = 2775 }, .{ .x = 2839, .y = 2773 }, .{ .x = 2838, .y = 2773 }, .{ .x = 2838, .y = 2772 }, .{ .x = 2834, .y = 2772 }, .{ .x = 2834, .y = 2774 }, .{ .x = 2833, .y = 2774 }, .{ .x = 2833, .y = 2775 }, .{ .x = 2834, .y = 2776 }, .{ .x = 2834, .y = 2777 }, .{ .x = 2835, .y = 2777 }, .{ .x = 2835, .y = 2778 }, .{ .x = 2837, .y = 2778 }, .{ .x = 2838, .y = 2777 }, .{ .x = 2838, .y = 2776 }, .{ .x = 2839, .y = 2776 }, .{ .x = 2839, .y = 2775 } }; +const C2_S39 = [_]Pt{ .{ .x = 2847, .y = 2798 }, .{ .x = 2841, .y = 2794 }, .{ .x = 2832, .y = 2806 }, .{ .x = 2847, .y = 2798 } }; +const C2_S40 = [_]Pt{ .{ .x = 2862, .y = 2800 }, .{ .x = 2864, .y = 2800 }, .{ .x = 2863, .y = 2799 }, .{ .x = 2863, .y = 2798 }, .{ .x = 2864, .y = 2797 }, .{ .x = 2860, .y = 2797 }, .{ .x = 2859, .y = 2798 }, .{ .x = 2859, .y = 2799 }, .{ .x = 2860, .y = 2800 }, .{ .x = 2862, .y = 2800 } }; +const C2_S41 = [_]Pt{ .{ .x = 2878, .y = 2810 }, .{ .x = 2878, .y = 2811 }, .{ .x = 2879, .y = 2810 }, .{ .x = 2878, .y = 2810 } }; +const C2_S42 = [_]Pt{ .{ .x = 2877, .y = 2810 }, .{ .x = 2876, .y = 2810 }, .{ .x = 2876, .y = 2813 }, .{ .x = 2877, .y = 2812 }, .{ .x = 2877, .y = 2810 } }; +const C2_S43 = [_]Pt{ .{ .x = 3271, .y = 2821 }, .{ .x = 3272, .y = 2828 }, .{ .x = 3286, .y = 2811 }, .{ .x = 3261, .y = 2808 }, .{ .x = 3271, .y = 2821 } }; +const C2_S44 = [_]Pt{ .{ .x = 3318, .y = 2817 }, .{ .x = 3319, .y = 2817 }, .{ .x = 3319, .y = 2816 }, .{ .x = 3317, .y = 2816 }, .{ .x = 3316, .y = 2817 }, .{ .x = 3316, .y = 2819 }, .{ .x = 3315, .y = 2819 }, .{ .x = 3315, .y = 2820 }, .{ .x = 3317, .y = 2820 }, .{ .x = 3317, .y = 2819 }, .{ .x = 3318, .y = 2819 }, .{ .x = 3318, .y = 2817 } }; +const C2_S45 = [_]Pt{ .{ .x = 2859, .y = 2837 }, .{ .x = 2859, .y = 2836 }, .{ .x = 2858, .y = 2836 }, .{ .x = 2858, .y = 2837 }, .{ .x = 2859, .y = 2837 } }; +const C2_S46 = [_]Pt{ .{ .x = 2862, .y = 2839 }, .{ .x = 2862, .y = 2838 }, .{ .x = 2861, .y = 2838 }, .{ .x = 2861, .y = 2839 }, .{ .x = 2862, .y = 2839 } }; +const C2_S47 = [_]Pt{ .{ .x = 2856, .y = 2892 }, .{ .x = 2856, .y = 2893 }, .{ .x = 2855, .y = 2893 }, .{ .x = 2855, .y = 2894 }, .{ .x = 2858, .y = 2894 }, .{ .x = 2859, .y = 2893 }, .{ .x = 2860, .y = 2893 }, .{ .x = 2860, .y = 2890 }, .{ .x = 2862, .y = 2888 }, .{ .x = 2864, .y = 2887 }, .{ .x = 2864, .y = 2885 }, .{ .x = 2863, .y = 2885 }, .{ .x = 2862, .y = 2886 }, .{ .x = 2862, .y = 2887 }, .{ .x = 2858, .y = 2891 }, .{ .x = 2856, .y = 2892 } }; +const C2_S48 = [_]Pt{ .{ .x = 2792, .y = 2898 }, .{ .x = 2793, .y = 2898 }, .{ .x = 2793, .y = 2896 }, .{ .x = 2792, .y = 2895 }, .{ .x = 2791, .y = 2895 }, .{ .x = 2791, .y = 2896 }, .{ .x = 2792, .y = 2897 }, .{ .x = 2792, .y = 2898 } }; +const C2_S49 = [_]Pt{ .{ .x = 2796, .y = 2898 }, .{ .x = 2796, .y = 2899 }, .{ .x = 2797, .y = 2898 }, .{ .x = 2797, .y = 2897 }, .{ .x = 2795, .y = 2897 }, .{ .x = 2795, .y = 2898 }, .{ .x = 2796, .y = 2898 } }; +const C2_S50 = [_]Pt{ .{ .x = 3205, .y = 2893 }, .{ .x = 3208, .y = 2898 }, .{ .x = 3208, .y = 2897 }, .{ .x = 3210, .y = 2896 }, .{ .x = 3207, .y = 2892 }, .{ .x = 3206, .y = 2890 }, .{ .x = 3205, .y = 2890 }, .{ .x = 3205, .y = 2891 }, .{ .x = 3204, .y = 2891 }, .{ .x = 3205, .y = 2893 } }; +const C2_S51 = [_]Pt{ .{ .x = 3204, .y = 2894 }, .{ .x = 3203, .y = 2892 }, .{ .x = 3202, .y = 2893 }, .{ .x = 3201, .y = 2893 }, .{ .x = 3200, .y = 2894 }, .{ .x = 3204, .y = 2900 }, .{ .x = 3206, .y = 2899 }, .{ .x = 3206, .y = 2898 }, .{ .x = 3204, .y = 2894 } }; +const C2_S52 = [_]Pt{ .{ .x = 3305, .y = 2829 }, .{ .x = 3305, .y = 2830 }, .{ .x = 3305, .y = 2831 }, .{ .x = 3306, .y = 2831 }, .{ .x = 3306, .y = 2830 }, .{ .x = 3305, .y = 2830 }, .{ .x = 3305, .y = 2829 } }; +const C2_S53 = [_]Pt{ .{ .x = 3270, .y = 2804 }, .{ .x = 3269, .y = 2804 }, .{ .x = 3269, .y = 2805 }, .{ .x = 3270, .y = 2805 }, .{ .x = 3270, .y = 2804 } }; +const C2_S54 = [_]Pt{ .{ .x = 3168, .y = 2793 }, .{ .x = 3166, .y = 2792 }, .{ .x = 3166, .y = 2793 }, .{ .x = 3162, .y = 2793 }, .{ .x = 3161, .y = 2794 }, .{ .x = 3160, .y = 2794 }, .{ .x = 3158, .y = 2796 }, .{ .x = 3157, .y = 2796 }, .{ .x = 3157, .y = 2797 }, .{ .x = 3156, .y = 2798 }, .{ .x = 3156, .y = 2800 }, .{ .x = 3157, .y = 2801 }, .{ .x = 3158, .y = 2801 }, .{ .x = 3158, .y = 2802 }, .{ .x = 3159, .y = 2801 }, .{ .x = 3161, .y = 2800 }, .{ .x = 3162, .y = 2800 }, .{ .x = 3165, .y = 2797 }, .{ .x = 3165, .y = 2796 }, .{ .x = 3167, .y = 2794 }, .{ .x = 3169, .y = 2794 }, .{ .x = 3169, .y = 2793 }, .{ .x = 3168, .y = 2793 } }; +const C2_S55 = [_]Pt{ .{ .x = 3357, .y = 2791 }, .{ .x = 3356, .y = 2790 }, .{ .x = 3356, .y = 2791 }, .{ .x = 3357, .y = 2791 } }; +const C2_S56 = [_]Pt{ .{ .x = 3188, .y = 2788 }, .{ .x = 3188, .y = 2787 }, .{ .x = 3188, .y = 2788 } }; +const C2_S57 = [_]Pt{ .{ .x = 3190, .y = 2787 }, .{ .x = 3192, .y = 2787 }, .{ .x = 3193, .y = 2786 }, .{ .x = 3194, .y = 2786 }, .{ .x = 3194, .y = 2785 }, .{ .x = 3192, .y = 2785 }, .{ .x = 3191, .y = 2786 }, .{ .x = 3190, .y = 2786 }, .{ .x = 3189, .y = 2787 }, .{ .x = 3190, .y = 2787 } }; +const C2_S58 = [_]Pt{ .{ .x = 3185, .y = 2775 }, .{ .x = 3190, .y = 2763 }, .{ .x = 3177, .y = 2776 }, .{ .x = 3174, .y = 2773 }, .{ .x = 3182, .y = 2767 }, .{ .x = 3176, .y = 2766 }, .{ .x = 3173, .y = 2783 }, .{ .x = 3178, .y = 2786 }, .{ .x = 3185, .y = 2775 } }; +const C2_S59 = [_]Pt{ .{ .x = 3124, .y = 2781 }, .{ .x = 3123, .y = 2781 }, .{ .x = 3123, .y = 2782 }, .{ .x = 3122, .y = 2782 }, .{ .x = 3122, .y = 2784 }, .{ .x = 3121, .y = 2784 }, .{ .x = 3121, .y = 2786 }, .{ .x = 3123, .y = 2784 }, .{ .x = 3124, .y = 2784 }, .{ .x = 3124, .y = 2781 } }; +const C2_S60 = [_]Pt{ .{ .x = 3198, .y = 2774 }, .{ .x = 3199, .y = 2774 }, .{ .x = 3199, .y = 2772 }, .{ .x = 3198, .y = 2772 }, .{ .x = 3197, .y = 2773 }, .{ .x = 3197, .y = 2774 }, .{ .x = 3198, .y = 2774 } }; +const C2_S61 = [_]Pt{ .{ .x = 3428, .y = 2757 }, .{ .x = 3427, .y = 2757 }, .{ .x = 3427, .y = 2758 }, .{ .x = 3428, .y = 2758 }, .{ .x = 3428, .y = 2757 } }; +const C2_S62 = [_]Pt{ .{ .x = 3128, .y = 2757 }, .{ .x = 3129, .y = 2756 }, .{ .x = 3129, .y = 2755 }, .{ .x = 3128, .y = 2755 }, .{ .x = 3128, .y = 2756 }, .{ .x = 3127, .y = 2757 }, .{ .x = 3126, .y = 2757 }, .{ .x = 3127, .y = 2758 }, .{ .x = 3127, .y = 2757 }, .{ .x = 3128, .y = 2757 } }; +const C2_S63 = [_]Pt{ .{ .x = 3138, .y = 2750 }, .{ .x = 3137, .y = 2750 }, .{ .x = 3137, .y = 2751 }, .{ .x = 3138, .y = 2751 }, .{ .x = 3138, .y = 2750 } }; +const C2_S64 = [_]Pt{ .{ .x = 3134, .y = 2750 }, .{ .x = 3134, .y = 2749 }, .{ .x = 3136, .y = 2747 }, .{ .x = 3135, .y = 2747 }, .{ .x = 3135, .y = 2748 }, .{ .x = 3134, .y = 2748 }, .{ .x = 3133, .y = 2748 }, .{ .x = 3133, .y = 2749 }, .{ .x = 3132, .y = 2749 }, .{ .x = 3132, .y = 2748 }, .{ .x = 3131, .y = 2748 }, .{ .x = 3131, .y = 2751 }, .{ .x = 3132, .y = 2751 }, .{ .x = 3134, .y = 2750 } }; +const C2_S65 = [_]Pt{ .{ .x = 3135, .y = 2746 }, .{ .x = 3134, .y = 2746 }, .{ .x = 3134, .y = 2747 }, .{ .x = 3135, .y = 2747 }, .{ .x = 3135, .y = 2746 } }; +const C2_S66 = [_]Pt{ .{ .x = 3138, .y = 2744 }, .{ .x = 3139, .y = 2743 }, .{ .x = 3140, .y = 2743 }, .{ .x = 3140, .y = 2742 }, .{ .x = 3139, .y = 2742 }, .{ .x = 3137, .y = 2744 }, .{ .x = 3137, .y = 2745 }, .{ .x = 3138, .y = 2745 }, .{ .x = 3138, .y = 2744 } }; +const C2_S67 = [_]Pt{ .{ .x = 3050, .y = 2731 }, .{ .x = 3050, .y = 2732 }, .{ .x = 3048, .y = 2732 }, .{ .x = 3048, .y = 2734 }, .{ .x = 3049, .y = 2734 }, .{ .x = 3050, .y = 2734 }, .{ .x = 3050, .y = 2731 } }; +const C2_S68 = [_]Pt{ .{ .x = 3127, .y = 2719 }, .{ .x = 3128, .y = 2718 }, .{ .x = 3126, .y = 2718 }, .{ .x = 3126, .y = 2719 }, .{ .x = 3127, .y = 2719 } }; +const C2_S69 = [_]Pt{ .{ .x = 3310, .y = 2716 }, .{ .x = 3308, .y = 2716 }, .{ .x = 3308, .y = 2717 }, .{ .x = 3309, .y = 2717 }, .{ .x = 3310, .y = 2716 } }; +const C2_S70 = [_]Pt{ .{ .x = 3370, .y = 2716 }, .{ .x = 3384, .y = 2708 }, .{ .x = 3382, .y = 2698 }, .{ .x = 3366, .y = 2704 }, .{ .x = 3370, .y = 2716 } }; +const C2_S71 = [_]Pt{ .{ .x = 3092, .y = 2711 }, .{ .x = 3091, .y = 2711 }, .{ .x = 3090, .y = 2712 }, .{ .x = 3090, .y = 2714 }, .{ .x = 3091, .y = 2714 }, .{ .x = 3091, .y = 2713 }, .{ .x = 3092, .y = 2713 }, .{ .x = 3092, .y = 2711 } }; +const C2_S72 = [_]Pt{ .{ .x = 3082, .y = 2712 }, .{ .x = 3081, .y = 2712 }, .{ .x = 3081, .y = 2713 }, .{ .x = 3083, .y = 2713 }, .{ .x = 3083, .y = 2712 }, .{ .x = 3082, .y = 2712 } }; +const C2_S73 = [_]Pt{ .{ .x = 3359, .y = 2703 }, .{ .x = 3359, .y = 2702 }, .{ .x = 3358, .y = 2702 }, .{ .x = 3358, .y = 2701 }, .{ .x = 3356, .y = 2701 }, .{ .x = 3356, .y = 2702 }, .{ .x = 3355, .y = 2702 }, .{ .x = 3353, .y = 2703 }, .{ .x = 3352, .y = 2703 }, .{ .x = 3349, .y = 2706 }, .{ .x = 3348, .y = 2706 }, .{ .x = 3346, .y = 2708 }, .{ .x = 3346, .y = 2709 }, .{ .x = 3348, .y = 2709 }, .{ .x = 3349, .y = 2710 }, .{ .x = 3350, .y = 2710 }, .{ .x = 3351, .y = 2711 }, .{ .x = 3354, .y = 2708 }, .{ .x = 3355, .y = 2708 }, .{ .x = 3357, .y = 2707 }, .{ .x = 3359, .y = 2705 }, .{ .x = 3359, .y = 2704 }, .{ .x = 3359, .y = 2703 } }; +const C2_S74 = [_]Pt{ .{ .x = 3271, .y = 2701 }, .{ .x = 3271, .y = 2700 }, .{ .x = 3272, .y = 2699 }, .{ .x = 3272, .y = 2700 }, .{ .x = 3271, .y = 2700 }, .{ .x = 3271, .y = 2699 }, .{ .x = 3270, .y = 2700 }, .{ .x = 3269, .y = 2700 }, .{ .x = 3268, .y = 2700 }, .{ .x = 3268, .y = 2701 }, .{ .x = 3269, .y = 2702 }, .{ .x = 3270, .y = 2702 }, .{ .x = 3271, .y = 2701 } }; +const C2_S75 = [_]Pt{ .{ .x = 3107, .y = 2700 }, .{ .x = 3107, .y = 2699 }, .{ .x = 3106, .y = 2699 }, .{ .x = 3106, .y = 2698 }, .{ .x = 3107, .y = 2698 }, .{ .x = 3107, .y = 2697 }, .{ .x = 3103, .y = 2697 }, .{ .x = 3101, .y = 2697 }, .{ .x = 3101, .y = 2698 }, .{ .x = 3100, .y = 2698 }, .{ .x = 3100, .y = 2699 }, .{ .x = 3101, .y = 2699 }, .{ .x = 3101, .y = 2700 }, .{ .x = 3103, .y = 2702 }, .{ .x = 3104, .y = 2702 }, .{ .x = 3105, .y = 2701 }, .{ .x = 3106, .y = 2701 }, .{ .x = 3106, .y = 2700 }, .{ .x = 3107, .y = 2700 } }; +const C2_S76 = [_]Pt{ .{ .x = 3366, .y = 2697 }, .{ .x = 3365, .y = 2697 }, .{ .x = 3365, .y = 2698 }, .{ .x = 3364, .y = 2698 }, .{ .x = 3364, .y = 2699 }, .{ .x = 3366, .y = 2699 }, .{ .x = 3366, .y = 2698 }, .{ .x = 3367, .y = 2698 }, .{ .x = 3367, .y = 2697 }, .{ .x = 3366, .y = 2697 } }; +const C2_S77 = [_]Pt{ .{ .x = 3362, .y = 2699 }, .{ .x = 3361, .y = 2700 }, .{ .x = 3361, .y = 2702 }, .{ .x = 3359, .y = 2702 }, .{ .x = 3360, .y = 2703 }, .{ .x = 3362, .y = 2703 }, .{ .x = 3363, .y = 2702 }, .{ .x = 3363, .y = 2700 }, .{ .x = 3362, .y = 2699 } }; +const C2_S78 = [_]Pt{ .{ .x = 3288, .y = 2675 }, .{ .x = 3273, .y = 2682 }, .{ .x = 3274, .y = 2695 }, .{ .x = 3287, .y = 2687 }, .{ .x = 3288, .y = 2675 } }; +const C2_S79 = [_]Pt{ .{ .x = 3329, .y = 2661 }, .{ .x = 3329, .y = 2660 }, .{ .x = 3328, .y = 2660 }, .{ .x = 3327, .y = 2661 }, .{ .x = 3327, .y = 2663 }, .{ .x = 3329, .y = 2663 }, .{ .x = 3329, .y = 2661 } }; +const C2_S80 = [_]Pt{ .{ .x = 3098, .y = 2659 }, .{ .x = 3099, .y = 2658 }, .{ .x = 3098, .y = 2657 }, .{ .x = 3096, .y = 2659 }, .{ .x = 3097, .y = 2659 }, .{ .x = 3097, .y = 2660 }, .{ .x = 3098, .y = 2659 } }; +const C2_S81 = [_]Pt{ .{ .x = 3277, .y = 2600 }, .{ .x = 3276, .y = 2599 }, .{ .x = 3276, .y = 2600 }, .{ .x = 3275, .y = 2600 }, .{ .x = 3275, .y = 2601 }, .{ .x = 3274, .y = 2601 }, .{ .x = 3274, .y = 2603 }, .{ .x = 3276, .y = 2603 }, .{ .x = 3277, .y = 2602 }, .{ .x = 3278, .y = 2602 }, .{ .x = 3278, .y = 2600 }, .{ .x = 3277, .y = 2600 } }; +const C2_S82 = [_]Pt{ .{ .x = 2926, .y = 2781 }, .{ .x = 2934, .y = 2773 }, .{ .x = 2919, .y = 2777 }, .{ .x = 2916, .y = 2785 }, .{ .x = 2926, .y = 2781 } }; +const C2_S83 = [_]Pt{ .{ .x = 2913, .y = 2779 }, .{ .x = 2914, .y = 2779 }, .{ .x = 2914, .y = 2777 }, .{ .x = 2912, .y = 2777 }, .{ .x = 2912, .y = 2778 }, .{ .x = 2913, .y = 2779 } }; +const C2_S84 = [_]Pt{ .{ .x = 2916, .y = 2773 }, .{ .x = 2916, .y = 2772 }, .{ .x = 2915, .y = 2772 }, .{ .x = 2914, .y = 2771 }, .{ .x = 2912, .y = 2771 }, .{ .x = 2912, .y = 2775 }, .{ .x = 2915, .y = 2775 }, .{ .x = 2916, .y = 2774 }, .{ .x = 2916, .y = 2773 } }; +const C2_S85 = [_]Pt{ .{ .x = 2912, .y = 2767 }, .{ .x = 2908, .y = 2762 }, .{ .x = 2907, .y = 2768 }, .{ .x = 2912, .y = 2767 } }; +const C2_S86 = [_]Pt{ .{ .x = 2949, .y = 2762 }, .{ .x = 2954, .y = 2763 }, .{ .x = 2950, .y = 2755 }, .{ .x = 2949, .y = 2762 } }; +const C2_S87 = [_]Pt{ .{ .x = 2946, .y = 2758 }, .{ .x = 2945, .y = 2758 }, .{ .x = 2945, .y = 2760 }, .{ .x = 2946, .y = 2760 }, .{ .x = 2946, .y = 2758 } }; +const C2_S88 = [_]Pt{ .{ .x = 2949, .y = 2753 }, .{ .x = 2948, .y = 2753 }, .{ .x = 2948, .y = 2754 }, .{ .x = 2949, .y = 2754 }, .{ .x = 2949, .y = 2753 } }; +const C2_S89 = [_]Pt{ .{ .x = 2923, .y = 2739 }, .{ .x = 2922, .y = 2739 }, .{ .x = 2921, .y = 2739 }, .{ .x = 2921, .y = 2740 }, .{ .x = 2922, .y = 2741 }, .{ .x = 2923, .y = 2741 }, .{ .x = 2923, .y = 2740 }, .{ .x = 2924, .y = 2740 }, .{ .x = 2923, .y = 2739 } }; +const C2_S90 = [_]Pt{ .{ .x = 2975, .y = 2460 }, .{ .x = 2975, .y = 2461 }, .{ .x = 2976, .y = 2460 }, .{ .x = 2975, .y = 2460 } }; +const C2_S91 = [_]Pt{ .{ .x = 2974, .y = 2502 }, .{ .x = 2974, .y = 2503 }, .{ .x = 2975, .y = 2503 }, .{ .x = 2975, .y = 2502 }, .{ .x = 2974, .y = 2502 } }; +const C2_S92 = [_]Pt{ .{ .x = 3077, .y = 2408 }, .{ .x = 3076, .y = 2408 }, .{ .x = 3076, .y = 2409 }, .{ .x = 3077, .y = 2408 } }; +const C2_S93 = [_]Pt{ .{ .x = 3256, .y = 2841 }, .{ .x = 3256, .y = 2840 }, .{ .x = 3258, .y = 2838 }, .{ .x = 3259, .y = 2839 }, .{ .x = 3257, .y = 2842 }, .{ .x = 3256, .y = 2841 } }; +const C2_S94 = [_]Pt{ .{ .x = 2918, .y = 2473 }, .{ .x = 2917, .y = 2473 }, .{ .x = 2917, .y = 2472 }, .{ .x = 2918, .y = 2472 }, .{ .x = 2918, .y = 2473 } }; +const C2_F0 = [_]Pt{ .{ .x = 2260, .y = 2591 }, .{ .x = 2300, .y = 2631 }, .{ .x = 2302, .y = 2633 }, .{ .x = 2303, .y = 2634 }, .{ .x = 2305, .y = 2636 }, .{ .x = 2391, .y = 2722 }, .{ .x = 2538, .y = 2868 }, .{ .x = 2578, .y = 2908 }, .{ .x = 2579, .y = 2909 }, .{ .x = 2580, .y = 2910 }, .{ .x = 2581, .y = 2911 }, .{ .x = 2637, .y = 2966 }, .{ .x = 2641, .y = 2970 }, .{ .x = 2642, .y = 2972 }, .{ .x = 2643, .y = 2972 }, .{ .x = 2648, .y = 2978 }, .{ .x = 2648, .y = 2978 }, .{ .x = 2740, .y = 3069 }, .{ .x = 2741, .y = 3070 }, .{ .x = 2741, .y = 3071 }, .{ .x = 2951, .y = 3279 }, .{ .x = 3450, .y = 2776 }, .{ .x = 3461, .y = 2765 }, .{ .x = 3443, .y = 2747 }, .{ .x = 3443, .y = 2747 }, .{ .x = 3439, .y = 2743 }, .{ .x = 3439, .y = 2743 }, .{ .x = 3437, .y = 2741 }, .{ .x = 3437, .y = 2741 }, .{ .x = 3435, .y = 2739 }, .{ .x = 3435, .y = 2738 }, .{ .x = 3430, .y = 2734 }, .{ .x = 3430, .y = 2733 }, .{ .x = 3428, .y = 2731 }, .{ .x = 3425, .y = 2729 }, .{ .x = 3425, .y = 2728 }, .{ .x = 3422, .y = 2726 }, .{ .x = 3404, .y = 2708 }, .{ .x = 3404, .y = 2707 }, .{ .x = 3403, .y = 2707 }, .{ .x = 3400, .y = 2704 }, .{ .x = 3390, .y = 2694 }, .{ .x = 3379, .y = 2683 }, .{ .x = 3378, .y = 2682 }, .{ .x = 3371, .y = 2674 }, .{ .x = 3369, .y = 2672 }, .{ .x = 3324, .y = 2628 }, .{ .x = 3322, .y = 2626 }, .{ .x = 3320, .y = 2624 }, .{ .x = 3319, .y = 2623 }, .{ .x = 3319, .y = 2623 }, .{ .x = 3318, .y = 2622 }, .{ .x = 3318, .y = 2622 }, .{ .x = 3298, .y = 2602 }, .{ .x = 3297, .y = 2601 }, .{ .x = 3295, .y = 2599 }, .{ .x = 3294, .y = 2598 }, .{ .x = 3293, .y = 2597 }, .{ .x = 3292, .y = 2596 }, .{ .x = 3292, .y = 2596 }, .{ .x = 3291, .y = 2595 }, .{ .x = 3291, .y = 2595 }, .{ .x = 3265, .y = 2569 }, .{ .x = 3264, .y = 2568 }, .{ .x = 3264, .y = 2568 }, .{ .x = 3263, .y = 2567 }, .{ .x = 3261, .y = 2565 }, .{ .x = 3259, .y = 2563 }, .{ .x = 3258, .y = 2562 }, .{ .x = 3239, .y = 2544 }, .{ .x = 3239, .y = 2543 }, .{ .x = 3097, .y = 2401 }, .{ .x = 3096, .y = 2401 }, .{ .x = 3090, .y = 2394 }, .{ .x = 3088, .y = 2393 }, .{ .x = 3088, .y = 2392 }, .{ .x = 3087, .y = 2391 }, .{ .x = 3086, .y = 2391 }, .{ .x = 3086, .y = 2390 }, .{ .x = 3085, .y = 2390 }, .{ .x = 3081, .y = 2386 }, .{ .x = 3078, .y = 2383 }, .{ .x = 3077, .y = 2382 }, .{ .x = 3077, .y = 2381 }, .{ .x = 3077, .y = 2381 }, .{ .x = 3076, .y = 2381 }, .{ .x = 3076, .y = 2381 }, .{ .x = 3075, .y = 2380 }, .{ .x = 3072, .y = 2377 }, .{ .x = 3072, .y = 2376 }, .{ .x = 3071, .y = 2376 }, .{ .x = 3071, .y = 2375 }, .{ .x = 3071, .y = 2375 }, .{ .x = 3070, .y = 2375 }, .{ .x = 3070, .y = 2374 }, .{ .x = 3054, .y = 2358 }, .{ .x = 3052, .y = 2356 }, .{ .x = 3051, .y = 2355 }, .{ .x = 3050, .y = 2355 }, .{ .x = 3050, .y = 2354 }, .{ .x = 3048, .y = 2353 }, .{ .x = 3048, .y = 2352 }, .{ .x = 3047, .y = 2352 }, .{ .x = 3047, .y = 2351 }, .{ .x = 3046, .y = 2350 }, .{ .x = 3045, .y = 2350 }, .{ .x = 3045, .y = 2349 }, .{ .x = 3045, .y = 2349 }, .{ .x = 3043, .y = 2347 }, .{ .x = 3042, .y = 2347 }, .{ .x = 3041, .y = 2346 }, .{ .x = 3041, .y = 2345 }, .{ .x = 3040, .y = 2344 }, .{ .x = 3040, .y = 2344 }, .{ .x = 3033, .y = 2337 }, .{ .x = 3031, .y = 2335 }, .{ .x = 3030, .y = 2335 }, .{ .x = 3030, .y = 2334 }, .{ .x = 3029, .y = 2333 }, .{ .x = 3028, .y = 2333 }, .{ .x = 2979, .y = 2284 }, .{ .x = 2967, .y = 2272 }, .{ .x = 2967, .y = 2271 }, .{ .x = 2956, .y = 2260 }, .{ .x = 2955, .y = 2260 }, .{ .x = 2920, .y = 2225 }, .{ .x = 2918, .y = 2223 }, .{ .x = 2917, .y = 2221 }, .{ .x = 2916, .y = 2221 }, .{ .x = 2909, .y = 2214 }, .{ .x = 2908, .y = 2213 }, .{ .x = 2884, .y = 2189 }, .{ .x = 2883, .y = 2187 }, .{ .x = 2882, .y = 2186 }, .{ .x = 2871, .y = 2176 }, .{ .x = 2860, .y = 2165 }, .{ .x = 2859, .y = 2164 }, .{ .x = 2845, .y = 2150 }, .{ .x = 2844, .y = 2149 }, .{ .x = 2843, .y = 2147 }, .{ .x = 2828, .y = 2133 }, .{ .x = 2770, .y = 2075 }, .{ .x = 2753, .y = 2093 }, .{ .x = 2749, .y = 2096 }, .{ .x = 2749, .y = 2097 }, .{ .x = 2749, .y = 2097 }, .{ .x = 2717, .y = 2129 }, .{ .x = 2573, .y = 2275 }, .{ .x = 2565, .y = 2283 }, .{ .x = 2560, .y = 2288 }, .{ .x = 2553, .y = 2295 }, .{ .x = 2551, .y = 2297 }, .{ .x = 2533, .y = 2315 }, .{ .x = 2532, .y = 2316 }, .{ .x = 2531, .y = 2317 }, .{ .x = 2519, .y = 2330 }, .{ .x = 2518, .y = 2330 } }; +const C2_F1 = [_]Pt{ .{ .x = 2753, .y = 2214 }, .{ .x = 2753, .y = 2209 }, .{ .x = 2753, .y = 2207 }, .{ .x = 2753, .y = 2204 }, .{ .x = 2753, .y = 2203 }, .{ .x = 2753, .y = 2201 }, .{ .x = 2753, .y = 2196 }, .{ .x = 2753, .y = 2187 }, .{ .x = 2753, .y = 2176 }, .{ .x = 2753, .y = 2175 }, .{ .x = 2753, .y = 2142 }, .{ .x = 2758, .y = 2142 }, .{ .x = 2764, .y = 2142 }, .{ .x = 2765, .y = 2142 }, .{ .x = 2767, .y = 2142 }, .{ .x = 2768, .y = 2142 }, .{ .x = 2835, .y = 2142 }, .{ .x = 2835, .y = 2155 }, .{ .x = 2835, .y = 2162 }, .{ .x = 2835, .y = 2168 }, .{ .x = 2835, .y = 2174 }, .{ .x = 2835, .y = 2176 }, .{ .x = 2835, .y = 2203 }, .{ .x = 2835, .y = 2207 }, .{ .x = 2835, .y = 2209 }, .{ .x = 2835, .y = 2214 }, .{ .x = 2821, .y = 2214 }, .{ .x = 2818, .y = 2214 }, .{ .x = 2795, .y = 2214 }, .{ .x = 2779, .y = 2214 } }; +const C2_SUBJECT = [_][]const Pt{ &C2_S0, &C2_S1, &C2_S2, &C2_S3, &C2_S4, &C2_S5, &C2_S6, &C2_S7, &C2_S8, &C2_S9, &C2_S10, &C2_S11, &C2_S12, &C2_S13, &C2_S14, &C2_S15, &C2_S16, &C2_S17, &C2_S18, &C2_S19, &C2_S20, &C2_S21, &C2_S22, &C2_S23, &C2_S24, &C2_S25, &C2_S26, &C2_S27, &C2_S28, &C2_S29, &C2_S30, &C2_S31, &C2_S32, &C2_S33, &C2_S34, &C2_S35, &C2_S36, &C2_S37, &C2_S38, &C2_S39, &C2_S40, &C2_S41, &C2_S42, &C2_S43, &C2_S44, &C2_S45, &C2_S46, &C2_S47, &C2_S48, &C2_S49, &C2_S50, &C2_S51, &C2_S52, &C2_S53, &C2_S54, &C2_S55, &C2_S56, &C2_S57, &C2_S58, &C2_S59, &C2_S60, &C2_S61, &C2_S62, &C2_S63, &C2_S64, &C2_S65, &C2_S66, &C2_S67, &C2_S68, &C2_S69, &C2_S70, &C2_S71, &C2_S72, &C2_S73, &C2_S74, &C2_S75, &C2_S76, &C2_S77, &C2_S78, &C2_S79, &C2_S80, &C2_S81, &C2_S82, &C2_S83, &C2_S84, &C2_S85, &C2_S86, &C2_S87, &C2_S88, &C2_S89, &C2_S90, &C2_S91, &C2_S92, &C2_S93, &C2_S94 }; +const C2_FACE = [_][]const Pt{ &C2_F0, &C2_F1 }; + +test "compose clip repro: dense sounding-hole DEPARE vs jagged face" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const before = boolean.open_chain_walks; + _ = try boolean.compute(a, &C2_SUBJECT, &C2_FACE, .intersect); + try std.testing.expectEqual(before, boolean.open_chain_walks); +} + +const OB_S0 = [_]Pt{ .{ .x = 2135, .y = -64 }, .{ .x = 2130, .y = 2 }, .{ .x = 2078, .y = 230 }, .{ .x = 2092, .y = 269 }, .{ .x = 2116, .y = 300 }, .{ .x = 2229, .y = 359 }, .{ .x = 2262, .y = 401 }, .{ .x = 2289, .y = 517 }, .{ .x = 2322, .y = 712 }, .{ .x = 2339, .y = 754 }, .{ .x = 2351, .y = 808 }, .{ .x = 2346, .y = 836 }, .{ .x = 2261, .y = 961 }, .{ .x = 2214, .y = 1121 }, .{ .x = 2202, .y = 1259 }, .{ .x = 2185, .y = 1331 }, .{ .x = 2127, .y = 1389 }, .{ .x = 2114, .y = 1419 }, .{ .x = 2109, .y = 1487 }, .{ .x = 2134, .y = 1582 }, .{ .x = 2155, .y = 1633 }, .{ .x = 2166, .y = 1642 }, .{ .x = 2190, .y = 1610 }, .{ .x = 2242, .y = 1564 }, .{ .x = 2343, .y = 1423 }, .{ .x = 2377, .y = 1357 }, .{ .x = 2403, .y = 1263 }, .{ .x = 2514, .y = 1064 }, .{ .x = 2525, .y = 1026 }, .{ .x = 2595, .y = 897 }, .{ .x = 2651, .y = 817 }, .{ .x = 2725, .y = 732 }, .{ .x = 2754, .y = 690 }, .{ .x = 2777, .y = 642 }, .{ .x = 2868, .y = 540 }, .{ .x = 2889, .y = 463 }, .{ .x = 3087, .y = 157 }, .{ .x = 3141, .y = 91 }, .{ .x = 3309, .y = -64 }, .{ .x = 3393, .y = -64 }, .{ .x = 3165, .y = 263 }, .{ .x = 3052, .y = 443 }, .{ .x = 2889, .y = 633 }, .{ .x = 2835, .y = 706 }, .{ .x = 2841, .y = 724 }, .{ .x = 2829, .y = 748 }, .{ .x = 2661, .y = 970 }, .{ .x = 2657, .y = 994 }, .{ .x = 2638, .y = 1021 }, .{ .x = 2621, .y = 1070 }, .{ .x = 2620, .y = 1094 }, .{ .x = 2629, .y = 1107 }, .{ .x = 2599, .y = 1143 }, .{ .x = 2561, .y = 1214 }, .{ .x = 2540, .y = 1287 }, .{ .x = 2470, .y = 1427 }, .{ .x = 2297, .y = 1578 }, .{ .x = 2176, .y = 1745 }, .{ .x = 2176, .y = 1764 }, .{ .x = 2210, .y = 1805 }, .{ .x = 2264, .y = 1936 }, .{ .x = 2282, .y = 1959 }, .{ .x = 2316, .y = 1968 }, .{ .x = 2346, .y = 1965 }, .{ .x = 2367, .y = 1954 }, .{ .x = 2399, .y = 1908 }, .{ .x = 2436, .y = 1877 }, .{ .x = 2490, .y = 1863 }, .{ .x = 2521, .y = 1867 }, .{ .x = 2560, .y = 1887 }, .{ .x = 2613, .y = 1942 }, .{ .x = 2646, .y = 1986 }, .{ .x = 2651, .y = 2012 }, .{ .x = 2631, .y = 2050 }, .{ .x = 2569, .y = 2107 }, .{ .x = 2541, .y = 2147 }, .{ .x = 2537, .y = 2188 }, .{ .x = 2555, .y = 2222 }, .{ .x = 2684, .y = 2299 }, .{ .x = 2727, .y = 2313 }, .{ .x = 2886, .y = 2333 }, .{ .x = 3015, .y = 2329 }, .{ .x = 3096, .y = 2316 }, .{ .x = 3202, .y = 2277 }, .{ .x = 3251, .y = 2247 }, .{ .x = 3276, .y = 2222 }, .{ .x = 3308, .y = 2162 }, .{ .x = 3321, .y = 2079 }, .{ .x = 3323, .y = 2006 }, .{ .x = 3289, .y = 1812 }, .{ .x = 3287, .y = 1749 }, .{ .x = 3291, .y = 1706 }, .{ .x = 3332, .y = 1530 }, .{ .x = 3361, .y = 1466 }, .{ .x = 3499, .y = 1215 }, .{ .x = 3578, .y = 1167 }, .{ .x = 3659, .y = 1149 }, .{ .x = 3781, .y = 1138 }, .{ .x = 3848, .y = 1152 }, .{ .x = 3913, .y = 1184 }, .{ .x = 3980, .y = 1241 }, .{ .x = 4124, .y = 1500 }, .{ .x = 4160, .y = 1526 }, .{ .x = 4160, .y = 2516 }, .{ .x = 3293, .y = 3252 }, .{ .x = 1897, .y = 1610 }, .{ .x = 1894, .y = 1573 }, .{ .x = 1808, .y = 1334 }, .{ .x = 1794, .y = 1225 }, .{ .x = 1769, .y = 1174 }, .{ .x = 1777, .y = 1092 }, .{ .x = 1768, .y = 1022 }, .{ .x = 1700, .y = 819 }, .{ .x = 1628, .y = 572 }, .{ .x = 1523, .y = -64 }, .{ .x = 1066, .y = -64 }, .{ .x = 1076, .y = -6 }, .{ .x = 1070, .y = 20 }, .{ .x = 940, .y = 230 }, .{ .x = 871, .y = 390 }, .{ .x = 490, .y = -64 } }; +const OB_F0 = [_]Pt{ .{ .x = 4160, .y = -64 }, .{ .x = 4160, .y = 2516 }, .{ .x = 3427, .y = 3139 }, .{ .x = 3293, .y = 3252 }, .{ .x = 3221, .y = 3167 }, .{ .x = 3206, .y = 3150 }, .{ .x = 3203, .y = 3145 }, .{ .x = 2458, .y = 2270 }, .{ .x = 2369, .y = 2166 }, .{ .x = 2191, .y = 1956 }, .{ .x = 2125, .y = 1879 }, .{ .x = 2109, .y = 1859 }, .{ .x = 2102, .y = 1851 }, .{ .x = 2099, .y = 1848 }, .{ .x = 2098, .y = 1846 }, .{ .x = 2095, .y = 1843 }, .{ .x = 2094, .y = 1842 }, .{ .x = 2092, .y = 1839 }, .{ .x = 2088, .y = 1835 }, .{ .x = 2062, .y = 1804 }, .{ .x = 2059, .y = 1801 }, .{ .x = 2058, .y = 1799 }, .{ .x = 2057, .y = 1798 }, .{ .x = 2057, .y = 1798 }, .{ .x = 2056, .y = 1797 }, .{ .x = 2050, .y = 1790 }, .{ .x = 2045, .y = 1784 }, .{ .x = 2044, .y = 1783 }, .{ .x = 2043, .y = 1782 }, .{ .x = 2043, .y = 1782 }, .{ .x = 2042, .y = 1781 }, .{ .x = 2018, .y = 1753 }, .{ .x = 1897, .y = 1610 }, .{ .x = 1851, .y = 1556 }, .{ .x = 1851, .y = 1556 }, .{ .x = 1829, .y = 1530 }, .{ .x = 1812, .y = 1510 }, .{ .x = 1669, .y = 1340 }, .{ .x = 871, .y = 390 }, .{ .x = 871, .y = 390 }, .{ .x = 490, .y = -64 } }; +const OB_SUBJECT = [_][]const Pt{&OB_S0}; +const OB_FACE = [_][]const Pt{&OB_F0}; + +test "compose clip repro: Pamlico DEPARE blob (retry-resistant)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + const before = boolean.open_chain_walks; + _ = try boolean.compute(a, &OB_SUBJECT, &OB_FACE, .intersect); + try std.testing.expectEqual(before, boolean.open_chain_walks); +} diff --git a/src/geometry/geometry.zig b/src/geometry/geometry.zig new file mode 100644 index 0000000..9190c75 --- /dev/null +++ b/src/geometry/geometry.zig @@ -0,0 +1,22 @@ +//! Integer computational geometry for chart composition. Pure (std-only), so the +//! module self-tests via `zig build test`. The scene baker and the tile +//! compositor use it to decide which cell owns each tile. +//! +//! * `boolean` — a Martinez–Rueda–Feito polygon boolean (union / intersection +//! / difference / symmetric-difference) on integer coordinates, with +//! overlap-edge typing and a deterministic total order, plus `unionAll`. +//! * `plane` — the per-tier coverage partition (`ownedAtTier`), the FULL / +//! EMPTY / SEAM tile classifier (`EdgeGrid`), and `clipLineOutsidePolys`. +//! * `partition` — the cell-ownership partition and its `.tpart` sidecar +//! (`serialize` / `deserialize` / `inputKey`). + +pub const boolean = @import("boolean.zig"); +pub const plane = @import("plane.zig"); +pub const partition = @import("partition.zig"); + +test { + _ = boolean; + _ = plane; + _ = partition; + _ = @import("boolean_repro_test.zig"); +} diff --git a/src/geometry/partition.zig b/src/geometry/partition.zig new file mode 100644 index 0000000..5070eac --- /dev/null +++ b/src/geometry/partition.zig @@ -0,0 +1,471 @@ +//! The per-band ownership partition — a stack of planar maps, one per distinct +//! band floor, each assigning every point to the cell that renders it at that +//! band. Built from a set of `plane.Cell` via `plane.ownedAtTierIndexed`: a finer +//! cell's face is its coverage minus the finer coverage, and two abutting same-band +//! cells split at their shared border by the DSID tie-break (so the internal border +//! dissolves). This is the artifact the compositor queries to clip each cell's tiles +//! to the ground it owns, and the debug view renders directly. +//! +//! A cell appears in the map of its native band with its full coverage, and in +//! coarser bands only where no coarser cell covers (a gap-filler); it is absent +//! from finer bands. So a "gap" in one band's map is not a bug — the ground is +//! owned by a coarser band, reached by querying that band's map. Pure geometry: no +//! S-57, no streaming, no allocator policy beyond a passed-in `gpa`. + +const std = @import("std"); +const plane = @import("plane.zig"); +const boolean = @import("boolean.zig"); + +/// Whether (date da, name na) orders strictly before (db, nb) in the equal-scale +/// clip order: newer DSID issue/update date first (YYYYMMDD compares lexically; a +/// dated cell orders before an undated one), then cell name ascending — total and +/// deterministic for distinct cells, so bake output is byte-stable. The baker and +/// this partition use the same tie-break so they pick the same ownership winner. +pub fn ordersBeforeKeys(da: []const u8, na: []const u8, db: []const u8, nb: []const u8) bool { + if (!std.mem.eql(u8, da, db)) return std.mem.lessThan(u8, db, da); // newer first + return std.mem.lessThan(u8, na, nb); +} + +pub const BandMap = struct { + /// The band floor (lowest zoom) this map is computed at. + tier: u8, + /// One face per cell owning ground at this band; `face.index` indexes + /// `Partition.cells`. `gpa`-owned. + faces: []plane.OwnedCell, + /// `n_cells`-long: global cell index → its slot in `faces`, or -1 if the cell + /// owns nothing at this band. Lets the compositor fetch a cell's face in O(1). + pos: []i32, +}; + +pub const Partition = struct { + gpa: std.mem.Allocator, + /// Borrowed — the caller keeps the cells (and their coverage) alive. + cells: []const plane.Cell, + /// Distinct band floors, DESCENDING: `maps[0]` is the finest band (highest + /// floor), `maps[len-1]` the coarsest. + tiers: []u8, + maps: []BandMap, + + pub fn deinit(self: *Partition) void { + for (self.maps) |m| { + plane.freeOwned(self.gpa, m.faces); + self.gpa.free(m.pos); + } + self.gpa.free(self.maps); + self.gpa.free(self.tiers); + } + + /// The map that governs zoom `z`: the finest band whose floor is ≤ z (i.e. the + /// largest tier ≤ z). A zoom below every floor resolves to the coarsest map, so + /// zooming out never falls off the bottom. null only if the partition is empty. + pub fn mapForZoom(self: *const Partition, z: u8) ?*const BandMap { + for (self.maps) |*m| { + if (m.tier <= z) return m; // tiers descending → first hit is finest applicable + } + if (self.maps.len > 0) return &self.maps[self.maps.len - 1]; + return null; + } + + /// The cell index that owns (x,y) at zoom `z`, or null — a true gap in this + /// band's map (the ground is owned by a coarser band). Coordinates are integers + /// (degrees × 10⁷). On-border results are even-odd-ambiguous; sample off edges. + pub fn ownerAt(self: *const Partition, z: u8, x: i64, y: i64) ?usize { + const m = self.mapForZoom(z) orelse return null; + for (m.faces) |f| { + if (boolean.pointInEvenOdd(f.owned, x, y)) return f.index; + } + return null; + } + + /// Cell `ci`'s owned face at the band governing zoom `z` — the rings (integer + /// lon/lat, degrees × 10⁷) of the ground it renders there, or null if it owns + /// nothing at that band. This is the region the compositor clips cell `ci`'s + /// features to before merging. + pub fn ownedFace(self: *const Partition, ci: usize, z: u8) ?[]const []const plane.Pt { + const m = self.mapForZoom(z) orelse return null; + if (ci >= m.pos.len) return null; + const slot = m.pos[ci]; + if (slot < 0) return null; + return m.faces[@intCast(slot)].owned; + } +}; + +/// Build the per-band ownership stack over `cells` (borrowed). One indexed partition +/// per distinct band floor. All face geometry is freshly allocated in `gpa`; free +/// with `Partition.deinit`. +pub fn build(gpa: std.mem.Allocator, cells: []const plane.Cell) !Partition { + // Distinct band floors. + var seen = std.AutoHashMap(u8, void).init(gpa); + defer seen.deinit(); + for (cells) |c| try seen.put(c.band_floor, {}); + + const tiers = try gpa.alloc(u8, seen.count()); + errdefer gpa.free(tiers); + { + var it = seen.keyIterator(); + var i: usize = 0; + while (it.next()) |k| : (i += 1) tiers[i] = k.*; + } + std.mem.sort(u8, tiers, {}, comptime std.sort.desc(u8)); // finest (highest floor) first + + const maps = try gpa.alloc(BandMap, tiers.len); + errdefer gpa.free(maps); + var built: usize = 0; + errdefer for (maps[0..built]) |m| { + plane.freeOwned(gpa, m.faces); + gpa.free(m.pos); + }; + for (tiers, 0..) |t, i| { + const faces = try plane.ownedAtTierIndexed(gpa, cells, t); + errdefer plane.freeOwned(gpa, faces); + const pos = try gpa.alloc(i32, cells.len); + @memset(pos, -1); + for (faces, 0..) |f, slot| pos[f.index] = @intCast(slot); + maps[i] = .{ .tier = t, .faces = faces, .pos = pos }; + built = i + 1; + } + + return .{ .gpa = gpa, .cells = cells, .tiers = tiers, .maps = maps }; +} + +// =========================================================================== +// Serialization — a precomputed partition as a self-contained sidecar. +// =========================================================================== +// +// The partition is a pure function of the cell set (each cell's coverage rings, +// cscl, band_floor, order, reach). Building it runs the expensive owned-face +// booleans; the RESULT is tiny (the whole owned-face geometry is ~1 MB for a +// district). So it is worth precomputing once and reusing: a cold recompose skips +// the build, and a runtime compositor can serve tiles from the sidecar alone. +// +// Format (little-endian, length-prefixed): +// magic "T57P" | version u32 | input_key u64 | n_cells u32 | n_maps u32 +// per map: tier u8 | n_faces u32 +// per face: cell_index u32 | n_rings u32 +// per ring: n_pts u32 | pts (x i64, y i64)… +// `pos` and `tiers` are reconstructed on load (pos from faces + n_cells). +// +// `input_key` binds the geometry to the exact cells it was built from: a loader +// recomputes it from the cells it holds and rejects a stale sidecar (→ rebuild), +// which is what makes an incremental recompose safe when coverage is unchanged. + +pub const MAGIC = [4]u8{ 'T', '5', '7', 'P' }; +pub const FORMAT_VERSION: u32 = 2; // 2: fill-up gap-filler faces (finer cells own uncovered ground at coarse tiers) + +pub const LoadError = error{ + BadMagic, + UnsupportedVersion, + StalePartition, // input_key mismatch — the cells changed; rebuild + CellCountMismatch, + Truncated, + OutOfMemory, +}; + +/// A hash of the exact inputs `build` consumes, so a stored partition can be +/// matched to the cells a loader holds. Coverage points are hashed as raw bytes; +/// both shipped targets are little-endian, and a false mismatch only forces a +/// (safe) rebuild. +fn hashPolyPoints(h: *std.hash.Wyhash, poly: plane.Poly) void { + for (poly) |ring| { + const n: u32 = @intCast(ring.len); + h.update(std.mem.asBytes(&n)); + h.update(std.mem.sliceAsBytes(ring)); // ring is []const Pt — real coordinate bytes + } +} + +pub fn inputKey(cells: []const plane.Cell) u64 { + var h = std.hash.Wyhash.init(0x5457_3537_5041_5254); // "TW57PART" + for (cells) |c| { + h.update(std.mem.asBytes(&c.cscl)); + h.update(std.mem.asBytes(&c.band_floor)); + h.update(std.mem.asBytes(&c.order)); + h.update(std.mem.asBytes(&c.reach)); + // cov1/cov2 are bags of polygons (each a set of rings). Descend to the POINTS and hash + // their bytes — never the ring slices' fat pointers, which are heap addresses that vary + // per process. Ring lengths go in too so a regrouping can't alias to the same digest. + for (c.cov1) |poly| hashPolyPoints(&h, poly); + h.update(&[_]u8{0xff}); // cov1/cov2 boundary marker + for (c.cov2) |poly| hashPolyPoints(&h, poly); + h.update(&[_]u8{0xfe}); // cell boundary marker + } + return h.final(); +} + +fn putInt(gpa: std.mem.Allocator, buf: *std.ArrayList(u8), comptime T: type, v: T) !void { + var tmp: [@sizeOf(T)]u8 = undefined; + std.mem.writeInt(T, &tmp, v, .little); + try buf.appendSlice(gpa, &tmp); +} + +/// Encode `part` to a fresh `gpa`-owned byte slice (free with `gpa.free`). +pub fn serialize(gpa: std.mem.Allocator, part: *const Partition) ![]u8 { + var buf = std.ArrayList(u8).empty; + errdefer buf.deinit(gpa); + try buf.appendSlice(gpa, &MAGIC); + try putInt(gpa, &buf, u32, FORMAT_VERSION); + try putInt(gpa, &buf, u64, inputKey(part.cells)); + try putInt(gpa, &buf, u32, @intCast(part.cells.len)); + try putInt(gpa, &buf, u32, @intCast(part.maps.len)); + for (part.maps) |m| { + try putInt(gpa, &buf, u8, m.tier); + try putInt(gpa, &buf, u32, @intCast(m.faces.len)); + for (m.faces) |f| { + try putInt(gpa, &buf, u32, @intCast(f.index)); + try putInt(gpa, &buf, u32, @intCast(f.owned.len)); + for (f.owned) |ring| { + try putInt(gpa, &buf, u32, @intCast(ring.len)); + for (ring) |p| { + try putInt(gpa, &buf, i64, p.x); + try putInt(gpa, &buf, i64, p.y); + } + } + } + } + return buf.toOwnedSlice(gpa); +} + +const Cursor = struct { + bytes: []const u8, + pos: usize = 0, + fn getInt(self: *Cursor, comptime T: type) !T { + const n = @sizeOf(T); + if (self.pos + n > self.bytes.len) return error.Truncated; + const v = std.mem.readInt(T, self.bytes[self.pos..][0..n], .little); + self.pos += n; + return v; + } +}; + +// Read one face (index + rings) fully, or free its partial geometry and error. +fn readFace(gpa: std.mem.Allocator, cur: *Cursor) !plane.OwnedCell { + const index: usize = @intCast(try cur.getInt(u32)); + const n_rings = try cur.getInt(u32); + const owned = try gpa.alloc([]plane.Pt, n_rings); + var built: usize = 0; + errdefer { + for (owned[0..built]) |r| gpa.free(r); + gpa.free(owned); + } + while (built < n_rings) : (built += 1) { + const n_pts = try cur.getInt(u32); + const ring = try gpa.alloc(plane.Pt, n_pts); + errdefer gpa.free(ring); + for (ring) |*p| p.* = .{ .x = try cur.getInt(i64), .y = try cur.getInt(i64) }; + owned[built] = ring; + } + return .{ .index = index, .owned = owned }; +} + +/// Decode a sidecar produced by `serialize` into a live `Partition` that borrows +/// `cells` (kept alive by the caller, exactly as `build` does). Rejects a sidecar +/// whose `input_key` does not match `cells` (the cells changed → rebuild). +pub fn deserialize(gpa: std.mem.Allocator, bytes: []const u8, cells: []const plane.Cell) LoadError!Partition { + if (bytes.len < MAGIC.len or !std.mem.eql(u8, bytes[0..MAGIC.len], &MAGIC)) return error.BadMagic; + var cur = Cursor{ .bytes = bytes, .pos = MAGIC.len }; + if (try cur.getInt(u32) != FORMAT_VERSION) return error.UnsupportedVersion; + if (try cur.getInt(u64) != inputKey(cells)) return error.StalePartition; + if (try cur.getInt(u32) != @as(u32, @intCast(cells.len))) return error.CellCountMismatch; + const n_maps = try cur.getInt(u32); + + const tiers = try gpa.alloc(u8, n_maps); + errdefer gpa.free(tiers); + const maps = try gpa.alloc(BandMap, n_maps); + errdefer gpa.free(maps); + var built_maps: usize = 0; + errdefer for (maps[0..built_maps]) |m| { + plane.freeOwned(gpa, m.faces); + gpa.free(m.pos); + }; + + for (0..n_maps) |i| { + const tier = try cur.getInt(u8); + const n_faces = try cur.getInt(u32); + const faces = try gpa.alloc(plane.OwnedCell, n_faces); + var built_faces: usize = 0; + errdefer { + for (faces[0..built_faces]) |f| boolean.freePolygon(gpa, f.owned); + gpa.free(faces); + } + while (built_faces < n_faces) : (built_faces += 1) { + faces[built_faces] = try readFace(gpa, &cur); + } + const pos = try gpa.alloc(i32, cells.len); + @memset(pos, -1); + for (faces, 0..) |f, slot| pos[f.index] = @intCast(slot); + tiers[i] = tier; + maps[i] = .{ .tier = tier, .faces = faces, .pos = pos }; + built_maps = i + 1; + } + + return .{ .gpa = gpa, .cells = cells, .tiers = tiers, .maps = maps }; +} + +// =========================================================================== +// Tests +// =========================================================================== + +const testing = std.testing; + +fn boxPoly(a: std.mem.Allocator, x0: i64, y0: i64, x1: i64, y1: i64) !plane.Poly { + const ring = try a.alloc(plane.Pt, 4); + ring[0] = .{ .x = x0, .y = y0 }; + ring[1] = .{ .x = x1, .y = y0 }; + ring[2] = .{ .x = x1, .y = y1 }; + ring[3] = .{ .x = x0, .y = y1 }; + const rings = try a.alloc([]const plane.Pt, 1); + rings[0] = ring; + return rings; +} + +test "partition band-stack: tiers descending, mapForZoom + ownerAt resolve per band" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Coarse [0,100]² at band floor 9 (coastal); harbor [40,60]² at floor 13 nested + // inside it. + const coarse = try boxPoly(a, 0, 0, 100, 100); + const harbor = try boxPoly(a, 40, 40, 60, 60); + const cells = [_]plane.Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{harbor} }, + }; + + var part = try build(testing.allocator, &cells); + defer part.deinit(); + + // Two tiers, finest (highest floor) first. + try testing.expectEqual(@as(usize, 2), part.tiers.len); + try testing.expectEqual(@as(u8, 13), part.tiers[0]); + try testing.expectEqual(@as(u8, 9), part.tiers[1]); + + // z14 → harbor band: harbor owns its box, coarse owns the surrounding ground. + try testing.expectEqual(@as(?usize, 1), part.ownerAt(14, 50, 50)); + try testing.expectEqual(@as(?usize, 0), part.ownerAt(14, 10, 10)); + // z10 → harbor is below its floor, so the coarse cell owns the whole basin. + try testing.expectEqual(@as(?usize, 0), part.ownerAt(10, 50, 50)); + // z3 → below every floor: resolves to the coarsest map, coarse still owns. + try testing.expectEqual(@as(?usize, 0), part.ownerAt(3, 50, 50)); + // Outside all coverage: a true gap. + try testing.expectEqual(@as(?usize, null), part.ownerAt(14, 200, 200)); + + // ownedFace hands the compositor a cell's owned geometry to clip against. + const hf = part.ownedFace(1, 14) orelse return error.TestUnexpectedResult; + try testing.expect(boolean.pointInEvenOdd(hf, 50, 50)); // harbor owns its box + const cf = part.ownedFace(0, 14) orelse return error.TestUnexpectedResult; + try testing.expect(boolean.pointInEvenOdd(cf, 10, 10)); // coarse owns the surround + try testing.expect(!boolean.pointInEvenOdd(cf, 50, 50)); // ...but NOT the harbor hole + try testing.expect(part.ownedFace(1, 10) == null); // harbor below its floor: owns nothing + try testing.expect(part.ownedFace(99, 14) == null); // out of range +} + +test "fill-up: finer cells own coarse-tier ground only where nothing coarser covers" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A coastal cell [0,100]² (floor 9) and two finer cells: a harbor [40,60]² + // (floor 13) inside the coastal coverage, and a harbor [200,220]² (floor 13) + // over ground NOTHING coarser covers. + const coastal = try boxPoly(a, 0, 0, 100, 100); + const harbor_in = try boxPoly(a, 40, 40, 60, 60); + const harbor_out = try boxPoly(a, 200, 200, 220, 220); + const cells = [_]plane.Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coastal} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{harbor_in} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 1, .cov1 = &.{harbor_out} }, + }; + + var part = try build(testing.allocator, &cells); + defer part.deinit(); + + // z4 → the coarsest map (tier 9). The coastal cell keeps every point it + // covers — the nested harbor is a gap-filler and takes NOTHING from it — + // while the uncovered harbor owns its own ground instead of a blank. + try testing.expectEqual(@as(?usize, 0), part.ownerAt(4, 50, 50)); + try testing.expectEqual(@as(?usize, 0), part.ownerAt(4, 10, 10)); + try testing.expectEqual(@as(?usize, 2), part.ownerAt(4, 210, 210)); + try testing.expectEqual(@as(?usize, null), part.ownerAt(4, 400, 400)); + + // At the harbor band the nested harbor owns its box as before. + try testing.expectEqual(@as(?usize, 1), part.ownerAt(14, 50, 50)); +} + +test "partition serialize/deserialize round-trips exactly" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const coarse = try boxPoly(a, 0, 0, 100, 100); + const harbor = try boxPoly(a, 40, 40, 60, 60); + const cells = [_]plane.Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{harbor} }, + }; + + var part = try build(testing.allocator, &cells); + defer part.deinit(); + + const bytes = try serialize(testing.allocator, &part); + defer testing.allocator.free(bytes); + + var got = try deserialize(testing.allocator, bytes, &cells); + defer got.deinit(); + + // Structural equality: tiers, per-map faces (IN ORDER — the compositor's tile + // iteration order rides on it), owned rings and their points. + try testing.expectEqualSlices(u8, part.tiers, got.tiers); + try testing.expectEqual(part.maps.len, got.maps.len); + for (part.maps, got.maps) |m0, m1| { + try testing.expectEqual(m0.tier, m1.tier); + try testing.expectEqualSlices(i32, m0.pos, m1.pos); + try testing.expectEqual(m0.faces.len, m1.faces.len); + for (m0.faces, m1.faces) |f0, f1| { + try testing.expectEqual(f0.index, f1.index); + try testing.expectEqual(f0.owned.len, f1.owned.len); + for (f0.owned, f1.owned) |r0, r1| { + try testing.expectEqualSlices(u8, std.mem.sliceAsBytes(r0), std.mem.sliceAsBytes(r1)); + } + } + } + + // Re-serializing the loaded partition yields the identical bytes. + const bytes2 = try serialize(testing.allocator, &got); + defer testing.allocator.free(bytes2); + try testing.expectEqualSlices(u8, bytes, bytes2); + + // Ownership queries agree with the freshly-built partition. + try testing.expectEqual(part.ownerAt(14, 50, 50), got.ownerAt(14, 50, 50)); + try testing.expectEqual(part.ownerAt(14, 10, 10), got.ownerAt(14, 10, 10)); + try testing.expectEqual(part.ownerAt(10, 50, 50), got.ownerAt(10, 50, 50)); +} + +test "partition sidecar rejects a stale cell set + a corrupt blob" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const coarse = try boxPoly(a, 0, 0, 100, 100); + const harbor = try boxPoly(a, 40, 40, 60, 60); + const cells = [_]plane.Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{harbor} }, + }; + var part = try build(testing.allocator, &cells); + defer part.deinit(); + const bytes = try serialize(testing.allocator, &part); + defer testing.allocator.free(bytes); + + // Different coverage → different input_key → rejected (→ caller rebuilds). + const harbor2 = try boxPoly(a, 41, 40, 60, 60); // one vertex moved + const cells2 = [_]plane.Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{harbor2} }, + }; + try testing.expectError(error.StalePartition, deserialize(testing.allocator, bytes, &cells2)); + + // A truncated blob and a bad magic are caught, not UB. + try testing.expectError(error.Truncated, deserialize(testing.allocator, bytes[0 .. bytes.len - 4], &cells)); + const bad = [_]u8{0} ** 8; + try testing.expectError(error.BadMagic, deserialize(testing.allocator, &bad, &cells)); +} diff --git a/src/geo/plane.zig b/src/geometry/plane.zig similarity index 58% rename from src/geo/plane.zig rename to src/geometry/plane.zig index e770af9..7aaa26e 100644 --- a/src/geo/plane.zig +++ b/src/geometry/plane.zig @@ -1,6 +1,6 @@ -//! Coverage-clipped best-available partition — the Stage-0 planar partition of -//! the cross-band composition redesign (specs/cross-band-composition-redesign.md, -//! Phase 0). "The finest cell whose M_COVR(CATCOV=1) covers a point owns that +//! Coverage-clipped best-available partition — the first-stage planar partition of +//! the cross-band chart composition. "The finest cell whose M_COVR(CATCOV=1) covers +//! a point owns that //! ground; every other cell is clipped away there." This module turns a set of //! ENC cells (each carrying a compilation scale, a band-eligibility floor, and //! coverage rings) into, per **zoom tier**, one `owned` region per cell — a @@ -12,17 +12,16 @@ //! basin owned by a below-floor fine cell (the HOLES blocker); recomputing per //! distinct floor (≤6 tiers) closes it geometrically. //! -//! Everything runs on the E7 integer lattice via `boolean.Pt` (i64) — coverage -//! coordinates are `round(deg·1e7)`, so seams that adjacent cells digitised -//! independently collapse to exact integer equality before any boolean runs. +//! Everything runs on integer coordinates via `boolean.Pt` (i64): coverage is +//! stored as degrees × 10⁷, so seams that adjacent cells digitised independently +//! round to the same integers before any boolean runs. //! All set algebra goes through `boolean` (Martinez, overlap-typed, deterministic). //! -//! Phase 0 scope: this computes and validates the partition, the tile classifier -//! (FULL / EMPTY / SEAM), and `clipLineOutsidePolys`. Nothing here is wired to the -//! baker or the live oracle yet — that is a later phase. The S-57 adapter (filling -//! `Cell` from an `s57.Cell`, the cscl≤0 / date-order tie-break decisions Q1/Q4) -//! is also deferred; `Cell` is deliberately decoupled so this stays pure and -//! unit-testable. +//! Scope: this computes and validates the partition, the tile classifier +//! (FULL / EMPTY / SEAM), and the coverage line clips. The S-57 adapter (filling +//! `Cell` from an `s57.Cell`, incl. the cscl≤0 and date-order tie-break policy) +//! lives with the consumers; `Cell` is deliberately decoupled so this stays pure +//! and unit-testable. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -32,7 +31,8 @@ pub const Pt = boolean.Pt; /// A polygon: a set of even-odd rings (exterior + holes). One M_COVR feature. pub const Poly = boolean.Polygon; -/// One ENC cell as the partition sees it. Coverage is already on the E7 lattice. +/// One ENC cell as the partition sees it. Coverage is already in integer +/// coordinates (degrees × 10⁷). /// `cov1`/`cov2` are bags of features that may mutually overlap — they are /// `unionAll`-cleaned into one simple region before use. pub const Cell = struct { @@ -42,12 +42,32 @@ pub const Cell = struct { band_floor: u8, /// Deterministic tie-break among equal-cscl cells — the adapter fills this /// from cell identity (issue/update date, then DSNM) so the newer survey wins - /// a double-owned strip (spec Q4). Lower sorts finer (earlier in the walk). + /// a double-owned strip. Lower sorts finer (earlier in the walk). order: u64, + /// Highest zoom this cell can EMIT tiles at (native window + overscale fill-up); + /// the cell is excluded from tiers beyond it. Its face there is dead weight — it + /// renders nothing there — and the exclusion is invisible to every renderable + /// face: the builders apply an EFFECTIVE reach (the max of the cell's own and + /// every coarser cell's, so exclusions always drop a suffix of the finest→ + /// coarsest walk and no kept cell's subtrahend changes — see the reach cut in + /// `ownedAtTier`). What it buys: the partition skips the expensive coarse-cell + /// booleans at fine tiers (a whole-district compose spent GBs subtracting + /// hundreds of harbour coverages from an overview cell that could never draw + /// there). Must be a true upper bound on the cell's emit zoom; the default (255) + /// never excludes — pure-geometry callers keep the full pool per tier. + reach: u8 = 255, /// CATCOV=1 coverage features. cov1: []const Poly, /// CATCOV=2 explicit no-data features (subtracted, so a coarser band can fill). cov2: []const Poly = &.{}, + /// Sector-figure reach (the archive's "light_reach" metadata): union bbox + /// [w,s,e,n] in DEGREES of the cell's figure-constructing light anchors, and + /// the max ground-length directional leg in metres. The compositor consults + /// the cell in tiles this ring touches even when it owns no ground there, so + /// legs/arcs don't amputate at the tile boundary. null = no figures. Carried + /// beside the partition inputs (never serialized — it rides the live cells). + light_bbox: ?[4]f64 = null, + light_range_m: f64 = 0, }; /// A cell's owned region at a tier: `index` into the caller's cell slice and the @@ -77,10 +97,30 @@ fn finerLess(_: void, a: Cell, b: Cell) bool { return a.order < b.order; } -/// The per-tier partition: for every cell eligible at `tier` (band_floor ≤ tier), -/// walking finest→coarsest, `owned = coverage \ (∪ coverage of all finer eligible -/// cells)`. The union of the returned `owned` regions equals the union of all -/// eligible coverages, partitioned with no overlap (validated by the tests). +/// The reach cap: shrink `order` (sorted finest→coarsest) to the cells whose +/// EFFECTIVE reach covers `tier`. Removing a cell only grows the faces of cells +/// AFTER it in the walk (their subtrahend shrinks), and a grown face is sound only +/// if that cell cannot render at this tier either — so the cut must be a SUFFIX of +/// the walk. Per-cell reach values need not be monotone in the sort key (a +/// scale-less cell sorts finest yet gets a mid-band default; a foreign archive can +/// carry tiles past its band window), so each cell's effective reach is the max of +/// its own and every coarser cell's: the drop is a suffix by construction, and +/// raising reach only KEEPS more cells, so nothing that could emit is ever cut. +fn reachCut(cells: []const Cell, order: *std.ArrayList(usize), tier: u8) void { + var keep = order.items.len; + var eff: u8 = 0; + while (keep > 0) { + eff = @max(eff, cells[order.items[keep - 1]].reach); + if (tier <= eff) break; + keep -= 1; + } + order.shrinkRetainingCapacity(keep); +} + +/// The per-tier partition: for every cell eligible at `tier` (band_floor ≤ tier ≤ +/// reach), walking finest→coarsest, `owned = coverage \ (∪ coverage of all finer +/// eligible cells)`. The union of the returned `owned` regions equals the union of +/// all eligible coverages, partitioned with no overlap (validated by the tests). pub fn ownedAtTier(gpa: Allocator, cells: []const Cell, tier: u8) ![]OwnedCell { // Eligible cells, in a total finest→coarsest, path-independent order. var order = std.ArrayList(usize).empty; @@ -93,6 +133,25 @@ pub fn ownedAtTier(gpa: Allocator, cells: []const Cell, tier: u8) ![]OwnedCell { return finerLess({}, cs[ia], cs[ib]); } }.lt); + reachCut(cells, &order, tier); + + // Fill-up gap-fillers: FINER cells (band_floor > tier) own ground at this + // coarser tier only where NO eligible cell covers — appended after every + // eligible cell, so they can never take ground from the band's own cells + // (position in `order` is priority: each cell's face is its coverage minus + // every earlier cell's). Among the fillers the COARSEST wins (closest to + // this tier's display scale), equal scales by the usual tie-break. So a + // harbor-only region still has an owner at z4, scamin-thinned by the bake. + const n_eligible = order.items.len; + for (cells, 0..) |c, i| { + if (c.band_floor > tier) try order.append(gpa, i); + } + std.mem.sort(usize, order.items[n_eligible..], cells, struct { + fn lt(cs: []const Cell, ia: usize, ib: usize) bool { + if (cs[ia].cscl != cs[ib].cscl) return cs[ia].cscl > cs[ib].cscl; // coarsest first + return cs[ia].order < cs[ib].order; + } + }.lt); var scratch = std.heap.ArenaAllocator.init(gpa); defer scratch.deinit(); @@ -107,14 +166,20 @@ pub fn ownedAtTier(gpa: Allocator, cells: []const Cell, tier: u8) ![]OwnedCell { // Accumulated coverage of all finer eligible cells processed so far. var covered: [][]Pt = try sa.alloc([]Pt, 0); - for (order.items) |i| { + for (order.items, 0..) |i, k| { const cov = try cellCoverage(sa, cells[i]); // owned = cov \ covered, allocated in gpa (the kept result). const owned = if (covered.len == 0) try dupePolygonGpa(gpa, cov) else try boolean.compute(gpa, cov, covered, .diff); - try out.append(gpa, .{ .index = i, .owned = owned }); + // A gap-filler fully covered by the eligible cells owns nothing here — + // drop its empty face so a nested finer cell adds no face at all. + if (k >= n_eligible and owned.len == 0) { + boolean.freePolygon(gpa, owned); + } else { + try out.append(gpa, .{ .index = i, .owned = owned }); + } // covered ∪= cov (scratch). const merged = try boolean.compute(sa, covered, cov, .unite); @@ -123,6 +188,123 @@ pub fn ownedAtTier(gpa: Allocator, cells: []const Cell, tier: u8) ![]OwnedCell { return out.toOwnedSlice(gpa); } +fn polyBbox(rings: []const []const Pt) [4]i64 { + var b = [4]i64{ std.math.maxInt(i64), std.math.maxInt(i64), std.math.minInt(i64), std.math.minInt(i64) }; + for (rings) |ring| for (ring) |p| { + b[0] = @min(b[0], p.x); + b[1] = @min(b[1], p.y); + b[2] = @max(b[2], p.x); + b[3] = @max(b[3], p.y); + }; + return b; +} + +fn bboxOverlap(a: [4]i64, b: [4]i64) bool { + return a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]; +} + +/// Identical result to `ownedAtTier`, built for scale. `ownedAtTier` accumulates a +/// GLOBAL union of every finer cell and differences each cell against it — the +/// operands grow to the whole nation, so it is O(cells²) in operand size (a +/// national district takes seconds). Here each cell is differenced only against +/// the finer eligible cells whose bounding box OVERLAPS it. A finer cell whose +/// bbox is disjoint cannot remove any area, so the result is the same partition +/// (cross-checked by the test below); charts overlap locally, so the per-cell +/// subtrahend stays small. Use this variant for real ENC data. +pub fn ownedAtTierIndexed(gpa: Allocator, cells: []const Cell, tier: u8) ![]OwnedCell { + var order = std.ArrayList(usize).empty; + defer order.deinit(gpa); + for (cells, 0..) |c, i| { + if (c.band_floor <= tier) try order.append(gpa, i); + } + std.mem.sort(usize, order.items, cells, struct { + fn lt(cs: []const Cell, ia: usize, ib: usize) bool { + return finerLess({}, cs[ia], cs[ib]); + } + }.lt); + reachCut(cells, &order, tier); + + // Fill-up gap-fillers: FINER cells (band_floor > tier) own ground at this + // coarser tier only where NO eligible cell covers — appended after every + // eligible cell, so they can never take ground from the band's own cells + // (position in `order` is priority: each cell's face is its coverage minus + // every earlier cell's). Among the fillers the COARSEST wins (closest to + // this tier's display scale), equal scales by the usual tie-break. So a + // harbor-only region still has an owner at z4, scamin-thinned by the bake. + const n_eligible = order.items.len; + for (cells, 0..) |c, i| { + if (c.band_floor > tier) try order.append(gpa, i); + } + std.mem.sort(usize, order.items[n_eligible..], cells, struct { + fn lt(cs: []const Cell, ia: usize, ib: usize) bool { + if (cs[ia].cscl != cs[ib].cscl) return cs[ia].cscl > cs[ib].cscl; // coarsest first + return cs[ia].order < cs[ib].order; + } + }.lt); + + var scratch = std.heap.ArenaAllocator.init(gpa); + defer scratch.deinit(); + const sa = scratch.allocator(); + + // Coverage + bbox per eligible cell (in finest→coarsest order), computed once. + const m = order.items.len; + const covs = try sa.alloc([][]Pt, m); + const bbs = try sa.alloc([4]i64, m); + for (order.items, 0..) |ci, k| { + covs[k] = try cellCoverage(sa, cells[ci]); + bbs[k] = polyBbox(covs[k]); + } + + var out = std.ArrayList(OwnedCell).empty; + errdefer { + for (out.items) |c| boolean.freePolygon(gpa, c.owned); + out.deinit(gpa); + } + + // Subtrahend pointer list, reused (cleared) each iteration — it only holds borrowed + // pointers into `covs`, so it stays tiny. The boolean intermediates below deliberately + // do NOT live in `sa`: it caches every cell's coverage for the whole loop, so scratch + // parked there would never be reclaimed — a coarse cell unions hundreds of finer + // cells, and every cell's union piling up was a whole-district compose's memory + // blow-up (tens of GB). + var subtr = std.ArrayList(Poly).empty; + defer subtr.deinit(gpa); + + for (order.items, 0..) |ci, k| { + // owned = cov \ (∪ finer cells whose bbox overlaps this one). The union/diff + // intermediates run on the page allocator, not `gpa`: they are large, + // odd-sized, and freed within the iteration, and a pooling gpa parks each + // freed size class on its own freelist — across hundreds of cells those + // parked pages inflated the compose's peak RSS by whole GBs. Page-granular + // allocation returns them to the OS at each free (unionAll frees its running + // accumulator each step, `uni`/`diff` right after use), and the boolean ops + // are far too coarse to feel the per-page cost. Only the finished `owned` + // polygon is copied into `gpa` (retained, held by the partition). + subtr.clearRetainingCapacity(); + for (0..k) |j| { + if (bboxOverlap(bbs[j], bbs[k])) try subtr.append(gpa, covs[j]); + } + const owned = if (subtr.items.len == 0) + try dupePolygonGpa(gpa, covs[k]) + else blk: { + const pa = std.heap.page_allocator; + const uni = try boolean.unionAll(pa, subtr.items); + defer boolean.freePolygon(pa, uni); + const diff = try boolean.compute(pa, covs[k], uni, .diff); + defer boolean.freePolygon(pa, diff); + break :blk try dupePolygonGpa(gpa, diff); + }; + // A gap-filler fully covered by the eligible cells owns nothing here — + // drop its empty face so a nested finer cell adds no face at all. + if (k >= n_eligible and owned.len == 0) { + boolean.freePolygon(gpa, owned); + continue; + } + try out.append(gpa, .{ .index = ci, .owned = owned }); + } + return out.toOwnedSlice(gpa); +} + fn dupePolygonGpa(gpa: Allocator, poly: Poly) ![][]Pt { const out = try gpa.alloc([]Pt, poly.len); errdefer gpa.free(out); @@ -160,14 +342,12 @@ fn pointInEvenOddF(rings: Poly, x: f64, y: f64) bool { return inside; } -/// Keep the parts of `line` that lie OUTSIDE `covered`. Each segment is split at -/// its integer crossings with every covered edge; a sub-run survives iff its -/// midpoint is outside `covered` (the finer cell owns the covered side and the -/// seam stroke). Returns a list of poly-lines allocated in `gpa`. -/// -/// This is the line analogue of the area difference — no polygon boolean, so a -/// coarse coastline inside finer coverage is dropped whole (not offset-doubled). -pub fn clipLineOutsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![][]Pt { +/// Split `line` at every integer crossing with `covered`'s edges and keep each +/// sub-run whose midpoint is on the wanted side — INSIDE `covered` when `keep_inside`, +/// else OUTSIDE. Returns a list of poly-lines allocated in `gpa`. The line analogue of +/// a polygon intersect/difference: no polygon boolean, so a line crossing the boundary +/// is cut cleanly, not offset-doubled. +fn clipLineByCoverage(gpa: Allocator, line: []const Pt, covered: Poly, keep_inside: bool) ![][]Pt { var out = std.ArrayList([]Pt).empty; errdefer { for (out.items) |r| gpa.free(r); @@ -211,8 +391,8 @@ pub fn clipLineOutsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![] } std.mem.sort(SplitPt, splits.items, {}, SplitPt.lt); - // Walk consecutive distinct split points; keep sub-runs whose midpoint is - // outside covered. + // Walk consecutive distinct split points; keep sub-runs whose midpoint is on + // the wanted side of `covered`. var prev = splits.items[0].p; if (run.items.len == 0) try run.append(gpa, prev); for (splits.items[1..]) |sp| { @@ -220,8 +400,8 @@ pub fn clipLineOutsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![] if (q.eql(prev)) continue; const mx = (@as(f64, @floatFromInt(prev.x)) + @as(f64, @floatFromInt(q.x))) / 2; const my = (@as(f64, @floatFromInt(prev.y)) + @as(f64, @floatFromInt(q.y))) / 2; - if (pointInEvenOddF(covered, mx, my)) { - // Sub-run is covered: flush the kept run (it ends at prev) and skip. + if (pointInEvenOddF(covered, mx, my) != keep_inside) { + // Sub-run is on the unwanted side: flush the kept run (ends at prev), skip. try flushRun(gpa, &out, &run); try run.append(gpa, q); } else { @@ -234,6 +414,18 @@ pub fn clipLineOutsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![] return out.toOwnedSlice(gpa); } +/// Keep the parts of `line` OUTSIDE `covered` — the parts a cell owns when `covered` +/// is the finer coverage that beats it (the seam stroke stays on the covered side). +pub fn clipLineOutsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![][]Pt { + return clipLineByCoverage(gpa, line, covered, false); +} + +/// Keep the parts of `line` INSIDE `covered` — clips a cell's line features to its +/// owned face at compose time (`covered` = the cell's owned region). +pub fn clipLineInsidePolys(gpa: Allocator, line: []const Pt, covered: Poly) ![][]Pt { + return clipLineByCoverage(gpa, line, covered, true); +} + const SplitPt = struct { key: i128, p: Pt, @@ -449,6 +641,117 @@ test "ownedAtTier: below-floor fine cell drops out of the pool (no blank window) try testing.expect(boolean.pointInEvenOdd(t10[0].owned, 10, 10)); } +test "reach cap: a cell beyond its tile reach drops out of finer tiers only" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Coarse [0,100]² can emit up to z10 (native max 9 + one fill-up); fine [40,60]² + // is unbounded. At tier 13 the coarse cell is out of the pool — the fine cell + // owns its box and NOTHING owns the surround; at tier 9 the coarse cell is back + // (fine below floor) and owns the whole basin. + const coarse_cov = try boxPoly(a, 0, 0, 100, 100); + const fine_cov = try boxPoly(a, 40, 40, 60, 60); + const cells = [_]Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .reach = 10, .cov1 = &.{coarse_cov} }, + .{ .cscl = 20_000, .band_floor = 13, .order = 0, .cov1 = &.{fine_cov} }, + }; + + for ([_]bool{ false, true }) |indexed| { + const t13 = if (indexed) try ownedAtTierIndexed(a, &cells, 13) else try ownedAtTier(a, &cells, 13); + try testing.expectEqual(@as(usize, 1), t13.len); + try testing.expectEqual(@as(usize, 1), t13[0].index); // only the fine cell + try testing.expect(boolean.pointInEvenOdd(t13[0].owned, 50, 50)); + // The fine cell's face is its own coverage, unchanged by the exclusion — + // it never subtracted against the (coarser) capped cell. + try testing.expect(!boolean.pointInEvenOdd(t13[0].owned, 10, 10)); + + const t9 = if (indexed) try ownedAtTierIndexed(a, &cells, 9) else try ownedAtTier(a, &cells, 9); + try testing.expectEqual(@as(usize, 1), t9.len); + try testing.expectEqual(@as(usize, 0), t9[0].index); // coarse back in the pool + try testing.expect(boolean.pointInEvenOdd(t9[0].owned, 50, 50)); + } +} + +test "reach cap: non-monotone reach cuts only a suffix (effective reach keeps the finer cell)" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Pathological input: the FINER cell has the LOWER reach (a scale-less cell that + // sorted finest but banded mid-ladder, or a foreign archive). Cutting it while the + // coarser cell stays would grow the coarse face over ground the finer cell used to + // mask — so the effective reach (max of own + all coarser) must keep it, and the + // partition must match the uncapped one exactly. + const coarse_cov = try boxPoly(a, 0, 0, 100, 100); + const fine_cov = try boxPoly(a, 40, 40, 60, 60); + const capped = [_]Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .reach = 255, .cov1 = &.{coarse_cov} }, + .{ .cscl = 20_000, .band_floor = 9, .order = 0, .reach = 10, .cov1 = &.{fine_cov} }, + }; + const uncapped = [_]Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse_cov} }, + .{ .cscl = 20_000, .band_floor = 9, .order = 0, .cov1 = &.{fine_cov} }, + }; + + for ([_]bool{ false, true }) |indexed| { + const got = if (indexed) try ownedAtTierIndexed(a, &capped, 13) else try ownedAtTier(a, &capped, 13); + const want = if (indexed) try ownedAtTierIndexed(a, &uncapped, 13) else try ownedAtTier(a, &uncapped, 13); + try testing.expectEqual(want.len, got.len); // both cells kept + // Same owner at a probe point inside the fine box and one outside it. + for ([_][2]i64{ .{ 50, 50 }, .{ 10, 10 } }) |p| { + var want_owner: ?usize = null; + var got_owner: ?usize = null; + for (want) |oc| if (boolean.pointInEvenOdd(oc.owned, p[0], p[1])) { + want_owner = oc.index; + }; + for (got) |oc| if (boolean.pointInEvenOdd(oc.owned, p[0], p[1])) { + got_owner = oc.index; + }; + try testing.expectEqual(want_owner, got_owner); + } + } +} + +test "ownedAtTierIndexed matches ownedAtTier (owner-at-point, overlap + adjacency)" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A mix: coarse [0,100]² with a nested fine [40,60]² (vertical overlap), plus + // two same-cscl [0,50]² / [50,100]² halves that abut (horizontal adjacency). + const coarse = try boxPoly(a, 0, 0, 100, 100); + const fine = try boxPoly(a, 40, 40, 60, 60); + const west = try boxPoly(a, 0, 0, 50, 100); + const east = try boxPoly(a, 50, 0, 100, 100); + const cells = [_]Cell{ + .{ .cscl = 100_000, .band_floor = 9, .order = 0, .cov1 = &.{coarse} }, + .{ .cscl = 20_000, .band_floor = 9, .order = 0, .cov1 = &.{fine} }, + .{ .cscl = 50_000, .band_floor = 9, .order = 0, .cov1 = &.{west} }, + .{ .cscl = 50_000, .band_floor = 9, .order = 1, .cov1 = &.{east} }, + }; + + const plain = try ownedAtTier(a, &cells, 9); + const idx = try ownedAtTierIndexed(a, &cells, 9); + + const ownerAt = struct { + fn f(faces: []const OwnedCell, x: i64, y: i64) ?usize { + for (faces) |oc| if (boolean.pointInEvenOdd(oc.owned, x, y)) return oc.index; + return null; + } + }.f; + + // Step 7 from 2 never lands on an edge (40/50/60/100), avoiding even-odd + // ambiguity; the two builders must name the same owner at every point. + var y: i64 = 2; + while (y < 100) : (y += 7) { + var x: i64 = 2; + while (x < 100) : (x += 7) { + try testing.expectEqual(ownerAt(plain, x, y), ownerAt(idx, x, y)); + } + } +} + test "fuzz: partition == per-point finest-eligible-covering, zero overlap, zero gap" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -487,7 +790,8 @@ test "fuzz: partition == per-point finest-eligible-covering, zero overlap, zero while (qy <= 11 * grid) : (qy += 19) { var qx: i64 = -7 * grid; while (qx <= 11 * grid) : (qx += 19) { - // Reference: finest (smallest cscl) eligible cell whose bbox covers. + // Reference: finest (smallest cscl) eligible cell whose bbox covers; + // where none covers, the COARSEST (largest cscl) finer cell fills up. var best: ?usize = null; for (cells.items, 0..) |c, i| { if (c.band_floor > tier) continue; @@ -496,6 +800,15 @@ test "fuzz: partition == per-point finest-eligible-covering, zero overlap, zero if (best == null or c.cscl < cells.items[best.?].cscl) best = i; } } + if (best == null) { + for (cells.items, 0..) |c, i| { + if (c.band_floor <= tier) continue; + const bb = bboxes.items[i]; + if (qx >= bb[0] and qx <= bb[2] and qy >= bb[1] and qy <= bb[3]) { + if (best == null or c.cscl > cells.items[best.?].cscl) best = i; + } + } + } // Skip points on any bbox edge (even-odd ambiguity). var on_edge = false; for (bboxes.items) |bb| { @@ -570,6 +883,21 @@ test "clipLineOutsidePolys: fully-covered line yields nothing; fully-outside lin try testing.expectEqual(@as(usize, 3), out_runs[0].len); } +test "clipLineInsidePolys: keeps the inside, drops the outside (compose clip-to-face)" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const covered = try boxPoly(a, 40, -10, 60, 10); // covers x∈[40,60] on the y=0 line + const line = [_]Pt{ .{ .x = 0, .y = 0 }, .{ .x = 100, .y = 0 } }; + const runs = try clipLineInsidePolys(a, &line, covered); + // Complement of the OUTSIDE clip: keep only the covered middle [40,60]. + try testing.expectEqual(@as(usize, 1), runs.len); + const r = runs[0]; + try testing.expectEqual(@as(i64, 40), @min(r[0].x, r[r.len - 1].x)); + try testing.expectEqual(@as(i64, 60), @max(r[0].x, r[r.len - 1].x)); +} + // Clip every coverage feature of a cell to `box` (a quadrant), dropping empties — // the operation the quadrant-partitioned Stage 0 applies before computing a // quadrant's partition locally. @@ -594,7 +922,7 @@ fn clipCellToBox(a: Allocator, cell: Cell, box: Box) !Cell { }; } -test "quadrant-split Stage 0 stitches to the same partition (shared E7 lattice)" { +test "quadrant-split partition stitches to the same result (shared integer grid)" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const a = arena.allocator(); @@ -602,7 +930,7 @@ test "quadrant-split Stage 0 stitches to the same partition (shared E7 lattice)" var prng = std.Random.DefaultPrng.init(0x9042BEEF); const rnd = prng.random(); const grid: i64 = 64; - // Split seam on the E7 lattice; cell coords are grid multiples so coverage + // Split seam on the integer grid; cell coords are grid multiples so coverage // edges meeting the seam collapse to exact equality. const sx: i64 = 0; const sy: i64 = 0; diff --git a/src/s57/iso8211.zig b/src/iso8211/iso8211.zig similarity index 100% rename from src/s57/iso8211.zig rename to src/iso8211/iso8211.zig diff --git a/src/lib_root.zig b/src/lib_root.zig index a8612fe..7ac6f64 100644 --- a/src/lib_root.zig +++ b/src/lib_root.zig @@ -13,7 +13,7 @@ pub const engine = @import("engine"); pub const api = @import("tile57.zig"); // the public Zig API (Chart/bake/style) pub const capi = @import("capi.zig"); pub const portray = @import("portray"); -pub const catalogue = @import("s100").catalogue; +pub const catalogue = @import("s101").catalogue; pub const bundle = @import("bundle"); // the chart-bundle pipeline (tile57_bake_bundle rides on capi) comptime { diff --git a/src/portray/portray.zig b/src/portray/portray.zig index 10e8ab2..77484ba 100644 --- a/src/portray/portray.zig +++ b/src/portray/portray.zig @@ -1,15 +1,15 @@ -//! Cell-driven S-101 portrayal. Adapts a cell's features (s101_adapt), exposes +//! Cell-driven S-101 portrayal. Adapts a cell's features (s101.adapter), exposes //! them to the embedded Lua via C-ABI accessors (tgp_*), runs the rules once //! (tg_portray_run in lua_shim.c), and returns per-feature instruction streams -//! for translation to MVT (s101_instr). +//! for translation to MVT (s101.instructions). //! //! Lib-only (links Lua via the C shim) — not part of the pure root.zig used by //! the tests/bake exe. const std = @import("std"); const s57 = @import("s57"); -const adapt = @import("s100").s101_adapt; -const catalogue = @import("s100").catalogue; // S-57 OBJL -> acronym for the quesmrk diagnostic +const adapter = @import("s101").adapter; +const catalogue = @import("s101").catalogue; // S-57 OBJL -> acronym for the quesmrk diagnostic // The embedded S-101 rules accessor (tg_embedded_lua) — referenced so its C-ABI // exports land in the compiled module; the Lua `require` searcher in lua_shim.c @@ -19,7 +19,7 @@ comptime { } const Ctx = struct { - adapted: []adapt.Adapted, + adapted: []adapter.Adapted, results: [][]const u8, // per adapted index -> instruction stream (arena-owned) arena: std.mem.Allocator, cell: *const s57.Cell, // for FFPT feature-to-feature association resolution @@ -285,7 +285,7 @@ pub const Context = struct { /// Run the S-101 rules over `adapted` with `ov`, returning a stream array indexed /// by cell.features index (null where the adapted subset doesn't cover a feature, /// or it emitted nothing). `features_len` is cell.features.len. -fn runAdapted(arena: std.mem.Allocator, cell: *const s57.Cell, adapted: []adapt.Adapted, rules_dir: []const u8, pctx: Context) ![]?[]const u8 { +fn runAdapted(arena: std.mem.Allocator, cell: *const s57.Cell, adapted: []adapter.Adapted, rules_dir: []const u8, pctx: Context) ![]?[]const u8 { const results = try arena.alloc([]const u8, adapted.len); for (results) |*r| r.* = ""; @@ -320,7 +320,7 @@ fn runAdapted(arena: std.mem.Allocator, cell: *const s57.Cell, adapted: []adapt. /// feature's S-101 instruction stream (or null if the class is unmapped / it /// emitted nothing). Allocates into `arena` (must outlive tile generation). pub fn portrayCell(arena: std.mem.Allocator, cell: *const s57.Cell, rules_dir: []const u8) ![]?[]const u8 { - const adapted = try adapt.adaptCell(arena, cell); + const adapted = try adapter.adaptCell(arena, cell); return runAdapted(arena, cell, adapted, rules_dir, .{}); } @@ -329,7 +329,7 @@ pub fn portrayCell(arena: std.mem.Allocator, cell: *const s57.Cell, rules_dir: [ /// boundary + point styles evaluate inside the rules, so the output needs none /// of the tile path's live-swap props. One pass, one context. pub fn portrayCellWith(arena: std.mem.Allocator, cell: *const s57.Cell, rules_dir: []const u8, ctx: Context) ![]?[]const u8 { - const adapted = try adapt.adaptCell(arena, cell); + const adapted = try adapter.adaptCell(arena, cell); return runAdapted(arena, cell, adapted, rules_dir, ctx); } @@ -345,29 +345,6 @@ pub const CellPortrayal = struct { simplified: ?[]const ?[]const u8 = null, }; -/// Portray only the features whose index is set in `mask` (indexed by -/// cell.features index): the default pass plus the SimplifiedSymbols variant -/// over the masked POINT features. Used by the scamin-standalone bake pre-pass -/// to portray just the deduped cross-cell winners — a small fraction of the -/// cell — without paying for a whole-cell portrayal twice. The whole cell is -/// still ADAPTED first (cheap, pure Zig) so cross-feature context (topmark -/// platforms etc.) matches the full-cell pass exactly; only the rule -/// evaluation is subset. Returned arrays are indexed by cell.features index. -pub fn portrayCellSubset(arena: std.mem.Allocator, cell: *const s57.Cell, rules_dir: []const u8, mask: []const bool) !CellPortrayal { - const adapted = try adapt.adaptCell(arena, cell); - var subset = std.ArrayList(adapt.Adapted).empty; - var points = std.ArrayList(adapt.Adapted).empty; - for (adapted) |ad| { - if (ad.feature_index >= mask.len or !mask[ad.feature_index]) continue; - try subset.append(arena, ad); - if (std.mem.eql(u8, ad.primitive, "Point")) try points.append(arena, ad); - } - var cp = CellPortrayal{ .base = try runAdapted(arena, cell, subset.items, rules_dir, .{}) }; - if (points.items.len > 0) - cp.simplified = runAdapted(arena, cell, points.items, rules_dir, .{ .simplified_symbols = true }) catch null; - return cp; -} - /// Portray a cell three ways so the client can toggle boundary style (areas) and /// point-symbol style (points) live: the default pass, a PlainBoundaries pass over /// only the area features, and a SimplifiedSymbols pass over only the point @@ -375,14 +352,14 @@ pub fn portrayCellSubset(arena: std.mem.Allocator, cell: *const s57.Cell, rules_ /// vary (areas for bnd, points for pts) — lines/soundings never read either /// override — so the extra rule evaluation is bounded to the relevant features. pub fn portrayCellVariants(arena: std.mem.Allocator, cell: *const s57.Cell, rules_dir: []const u8) !CellPortrayal { - const adapted = try adapt.adaptCell(arena, cell); + const adapted = try adapter.adaptCell(arena, cell); const base = try runAdapted(arena, cell, adapted, rules_dir, .{}); // Partition the adapted features by the variant they can contribute. "Surface" // → the plain-boundary variant; "Point" → the simplified-symbol variant // (SOUNDG is already excluded from `adapted`, and "Curve" varies under neither). - var areas = std.ArrayList(adapt.Adapted).empty; - var points = std.ArrayList(adapt.Adapted).empty; + var areas = std.ArrayList(adapter.Adapted).empty; + var points = std.ArrayList(adapter.Adapted).empty; for (adapted) |ad| { if (std.mem.eql(u8, ad.primitive, "Surface")) { try areas.append(arena, ad); diff --git a/src/render/ascii.zig b/src/render/ascii.zig index f9d95c3..54edbb4 100644 --- a/src/render/ascii.zig +++ b/src/render/ascii.zig @@ -16,7 +16,7 @@ //! Buffering exists for the same reason as pixel.zig: the engine emits //! features in CELL order, but a grid must be WRITTEN in S-52 priority order. //! -//! Rule (surface.zig): no s57/s100/portray imports — everything a character +//! Rule (surface.zig): no s57/s101/portray imports — everything a character //! needs is already on the call. const std = @import("std"); @@ -106,7 +106,7 @@ pub const AsciiSurface = struct { a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, - settings: *const resolve.MarinerSettings, + settings: *const resolve.Settings, /// Fractional display zoom the gates evaluate at. zoom: f64, /// Output size in characters. @@ -141,7 +141,7 @@ pub const AsciiSurface = struct { /// `a` should be a scratch arena, like PixelSurface: buffered ops live /// until endScene. `px_per_tile` is the view driver's usual /// 256 * 2^(zoom - round(zoom)) — one character column plays one CSS px. - pub fn initView(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, zoom: f64, cols: u32, rows: u32, px_per_tile: f32, tile_extent: u32) AsciiSurface { + pub fn initView(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.Settings, zoom: f64, cols: u32, rows: u32, px_per_tile: f32, tile_extent: u32) AsciiSurface { return .{ .a = a, .colors = colors, @@ -577,7 +577,7 @@ test "AsciiSurface: fills shade by token, strokes pick slope chars, glyphs and l const a = arena.allocator(); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; // 32x16 chars = a 32x32 px canvas; tile extent 32 makes tile units = px. var as = AsciiSurface.initView(a, &colors, .day, &settings, 14.0, 32, 16, 32, 32); const surf = as.asSurface(); @@ -635,7 +635,7 @@ test "labels declutter: highest priority claims the cells, overlap is dropped" { const a = arena.allocator(); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var as = AsciiSurface.initView(a, &colors, .day, &settings, 14.0, 32, 16, 32, 32); const surf = as.asSurface(); @@ -667,7 +667,7 @@ test "ANSI mode: tokens quantize to xterm-256, rows reset, plain mode stays esca try std.testing.expectEqual(@as(u8, 243), ansi256(.{ .r = 128, .g = 128, .b = 128 })); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var as = AsciiSurface.initView(a, &colors, .day, &settings, 14.0, 8, 4, 8, 8); as.ansi = true; const surf = as.asSurface(); diff --git a/src/render/ascii_view_test.zig b/src/render/ascii_view_test.zig index 292f375..d83c40d 100644 --- a/src/render/ascii_view_test.zig +++ b/src/render/ascii_view_test.zig @@ -85,7 +85,7 @@ test "ascii view: water shades left, land '#' right, coastline between" { const streams = try portray.portrayCell(a, &cell, ""); var colors = try render.resolve.Colors.init(a, colorprofile_registry.entries[0].bytes); - const settings = render.resolve.MarinerSettings{}; + const settings = render.resolve.Settings{}; // 64x32 chars = a 64x64 px view at z8 (px_per_tile 256), centred on the // tile: the whole grid sits inside the fixture, split down the middle. const cols: u32 = 64; diff --git a/src/render/inspect.zig b/src/render/inspect.zig index 2494154..549e20e 100644 --- a/src/render/inspect.zig +++ b/src/render/inspect.zig @@ -8,7 +8,7 @@ //! It is the mirror image of noop.zig (which discards every call) and ascii.zig //! (which resolves each call to a character): here the recording IS the output. //! -//! Same rule as every surface (surface.zig): no s57 / s100 / portray imports. +//! Same rule as every surface (surface.zig): no s57 / s101 / portray imports. //! The FeatureMeta the engine hands beginFeature already carries the S-57 class //! acronym + the acronym→value pick blob, and the draw calls carry color tokens / //! symbol names verbatim — everything the record needs is on the contract. diff --git a/src/render/inspect_view_test.zig b/src/render/inspect_view_test.zig index 08de6da..939bef2 100644 --- a/src/render/inspect_view_test.zig +++ b/src/render/inspect_view_test.zig @@ -5,7 +5,7 @@ //! CLI performs: //! //! 1. raw S-57 — FeatureMeta.class / .s57_json on the recorded feature -//! 2. S-101 stream — portray.portrayCell → s101_instr.parse +//! 2. S-101 stream — portray.portrayCell → s101.instructions.parse //! 3. resolved calls — the Surface calls the InspectSurface captured //! //! End-to-end seam: portray -> instruction parse -> geometry/clip -> Surface @@ -120,7 +120,7 @@ test "inspect view: real rules -> InspectSurface records the 3 levels per featur try std.testing.expect(std.mem.startsWith(u8, depare_fill.?, "DEP")); // a depth-shade token // Its level-2 stream parses to a fill instruction (ColorFill:DEP..). - const depare_parsed = try @import("s100").s101_instr.parse(a, streams[0].?); + const depare_parsed = try @import("s101").instructions.parse(a, streams[0].?); try std.testing.expect(depare_parsed.fill_token != null); // --- COALNE: a line feature resolves to a stroke. --- @@ -137,7 +137,7 @@ test "inspect view: real rules -> InspectSurface records the 3 levels per featur const buoy = byClass(&is, "BOYLAT") orelse return error.NoBuoy; try std.testing.expect(std.mem.indexOf(u8, buoy.meta.s57_json, "OBJNAM") != null); // level 1 - const buoy_parsed = try @import("s100").s101_instr.parse(a, streams[2].?); + const buoy_parsed = try @import("s101").instructions.parse(a, streams[2].?); try std.testing.expect(buoy_parsed.points.len >= 1); // level 2: a symbol instruction var buoy_symbol: ?[]const u8 = null; diff --git a/src/render/pixel.zig b/src/render/pixel.zig index a479c72..e681165 100644 --- a/src/render/pixel.zig +++ b/src/render/pixel.zig @@ -86,7 +86,7 @@ pub const PixelSurface = struct { a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, - settings: *const resolve.MarinerSettings, + settings: *const resolve.Settings, /// Fractional display zoom the gates evaluate at. zoom: f64, /// Output size in pixels; a tile render is square (w == h == px_per_tile), @@ -138,13 +138,13 @@ pub const PixelSurface = struct { /// `a` should be the same scratch arena the engine allocates geometry /// from — buffered ops live until endScene, exactly like the tile /// surface's feature lists. - pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, zoom: f64, size_px: u32, tile_extent: u32) PixelSurface { + pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.Settings, zoom: f64, size_px: u32, tile_extent: u32) PixelSurface { return initView(a, colors, palette, settings, zoom, size_px, size_px, @floatFromInt(size_px), tile_extent); } /// A multi-tile view scene: w x h output px, each source tile occupying /// px_per_tile px (fractional zoom = a non-power-of-two px_per_tile). - pub fn initView(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, zoom: f64, w_px: u32, h_px: u32, px_per_tile: f32, tile_extent: u32) PixelSurface { + pub fn initView(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.Settings, zoom: f64, w_px: u32, h_px: u32, px_per_tile: f32, tile_extent: u32) PixelSurface { return .{ .a = a, .colors = colors, @@ -257,7 +257,7 @@ pub const PixelSurface = struct { try self.push(.line, .{ .stroke = .{ .lines = canvas_lines, .width = w, .dash = d, .color = self.resolveColor(token) } }); // Depth-contour label: the value in the mariner's unit, placed at the // midpoint of the longest segment (v1 horizontal placement; collision - // culls the rest). Value formatting mirrors chartstyle.contourLabelField: + // culls the rest). Value formatting mirrors mariner.contourLabelField: // metres round, feet floor-to-tenth (a depth errs SHALLOW). if (valdco) |v| { var longest: f32 = 0; @@ -296,7 +296,7 @@ pub const PixelSurface = struct { const store = self.store orelse return; // Re-gate with the symbol name: ISODGR01 rides its own toggle. if (!resolve.visible(&self.cur, name, self.zoom, self.settings)) return; - // Live danger swap (mirrors chartstyle.pointSymbolImage): a danger lying + // Live danger swap (mirrors mariner.pointSymbolImage): a danger lying // DEEPER than the mariner's safety contour draws the subdued DANGER02. var eff = name; if (danger_depth) |dd| eff = if (dd > self.settings.safety_contour) "DANGER02" else "DANGER01"; @@ -617,7 +617,7 @@ test "PixelSurface: resolves tokens, gates SCAMIN, sorts by draw_prio" { const a = arena.allocator(); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var ps = PixelSurface.init(a, &colors, .day, &settings, 14.0, 64, 64); const surf = ps.asSurface(); @@ -662,7 +662,7 @@ test "PixelSurface: unknown token falls back magenta, dashed maps to [4w,3w]" { const a = arena.allocator(); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var ps = PixelSurface.init(a, &colors, .day, &settings, 14.0, 64, 4096); const surf = ps.asSurface(); @@ -718,7 +718,7 @@ test "drawSymbol: pivot/scale/rotate transform, even-odd fill, danger swap, soun }; var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{ .display_other = true }; + const settings = resolve.Settings{ .display_other = true }; var ps = PixelSurface.init(a, &colors, .day, &settings, 14.0, 256, 256); ps.store = .{ .ptr = &fake, .vtable = &Fake.vt }; const surf = ps.asSurface(); @@ -764,7 +764,7 @@ test "drawText: shaping, group gate, halo, and collision declutter" { const a = arena.allocator(); var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var ps = PixelSurface.init(a, &colors, .day, &settings, 14.0, 256, 256); const surf = ps.asSurface(); @@ -788,7 +788,7 @@ test "drawText: shaping, group gate, halo, and collision declutter" { try surf.drawText("Overlap", &style, .{ .x = 102, .y = 101 }); try surf.endFeature(); // A light description with the toggle OFF: gated before buffering. - const no_lights = resolve.MarinerSettings{ .show_light_descriptions = false }; + const no_lights = resolve.Settings{ .show_light_descriptions = false }; var ps2 = PixelSurface.init(a, &colors, .day, &no_lights, 14.0, 256, 256); const lstyle = rs.TextStyle{ .color = "CHBLK", .font_size = 11, .halign = "left", .valign = "bottom", .offset_x = 0, .offset_y = 0, .group = 23 }; try ps2.asSurface().beginFeature(&hi); @@ -834,7 +834,7 @@ test "drawHighlight: reticle straddles the anchor, bbox adds a dashed box, hi-vi }; var colors = try resolve.Colors.init(a, test_profile); - const settings = resolve.MarinerSettings{}; + const settings = resolve.Settings{}; var ps = PixelSurface.init(a, &colors, .day, &settings, 14.0, 400, 400); // POINT: reticle only (no dashed box). Both halo + core passes straddle the diff --git a/src/render/pixel_golden_test.zig b/src/render/pixel_golden_test.zig index 92e0494..a154075 100644 --- a/src/render/pixel_golden_test.zig +++ b/src/render/pixel_golden_test.zig @@ -78,7 +78,7 @@ test "golden PNG: depth-area fill + coastline stroke through the pixel path" { const streams = try portray.portrayCell(a, &cell, ""); var colors = try render.resolve.Colors.init(a, colorprofile_registry.entries[0].bytes); - const settings = render.resolve.MarinerSettings{}; + const settings = render.resolve.Settings{}; var ps = render.pixel.PixelSurface.init(a, &colors, .day, &settings, @floatFromInt(z), 256, tile.EXTENT); const cells = [_]scene.CellRef{.{ .cell = &cell, .portrayal = streams, .geo = geo }}; diff --git a/src/render/render.zig b/src/render/render.zig index 1d62f53..99839c4 100644 --- a/src/render/render.zig +++ b/src/render/render.zig @@ -16,7 +16,7 @@ //! ascii — AsciiSurface: the chart as a Unicode text grid (optional //! ANSI-256 color). The worked example of adding a backend. //! -//! Rule: nothing in this module imports s57, s100, or portray. If a surface +//! Rule: nothing in this module imports s57, s101, or portray. If a surface //! needs a fact the Surface calls don't carry, that's an engine bug — extend //! the contract, never back-channel. diff --git a/src/render/resolve.zig b/src/render/resolve.zig index d317aa4..3c992d8 100644 --- a/src/render/resolve.zig +++ b/src/render/resolve.zig @@ -4,17 +4,17 @@ //! SCAMIN) evaluated at the scene zoom. //! //! Tile surfaces (MVT/MLT) never resolve: they serialize tokens/names verbatim -//! and the MapLibre client resolves live (assets/colortables.json + the -//! chartstyle expressions). The resolver mirrors those exact semantics so the +//! and the MapLibre client resolves live (the color tables + the +//! mariner expressions). The resolver mirrors those exact semantics so the //! two styling paths can't silently drift; each gate cites the expression it //! mirrors. const std = @import("std"); const Allocator = std.mem.Allocator; const rs = @import("surface.zig"); -const chartstyle = @import("assets").chartstyle; +const mariner = @import("style").mariner; -pub const MarinerSettings = chartstyle.MarinerSettings; +pub const Settings = mariner.Settings; // ---- colors --------------------------------------------------------------- @@ -24,8 +24,8 @@ pub const PaletteId = enum(u2) { day, dusk, night }; pub const Rgb = struct { r: u8, g: u8, b: u8 }; /// Token -> RGB for all three palettes, parsed once from colorProfile.xml — -/// the same source assets.colorTablesJson serializes for the MapLibre client -/// (parse mirrored from src/assets/assets.zig; keep in sync). Token keys are +/// the same source style.colorTablesJson serializes for the MapLibre client +/// (parse mirrored from src/style/style.zig; keep in sync). Token keys are /// slices INTO `xml`, so the xml must outlive the Colors (the embedded /// catalogue profile is static, so this is free in practice). pub const Colors = struct { @@ -57,7 +57,7 @@ pub const Colors = struct { // Read the decimal byte inside the first NNN within `s` — the // // children of an 's block (unambiguous: the -// sibling block carries //). Mirrors assets.zig tagByte. +// sibling block carries //). Mirrors style.zig tagByte. fn tagByte(s: []const u8, comptime tag: []const u8) ?u8 { const open = "<" ++ tag ++ ">"; const i = std.mem.indexOf(u8, s, open) orelse return null; @@ -96,14 +96,21 @@ fn collectItems(a: Allocator, block: []const u8, map: *std.StringHashMapUnmanage // ---- display gates ---------------------------------------------------------- -/// S-52 §10.3.4 display-category gate — mirrors chartstyle.categoryFilter: +/// S-52 §10.3.4 display-category gate — mirrors mariner.categoryFilter: /// the effective category is the feature's `cat` (0 base / 1 standard / /// 2 other; null defaults to standard, like the style's coalesce), except /// ISODGR01 which rides the isolated-dangers-shallow toggle instead of its /// baked category. M_QUAL is the data-quality overlay: shown iff the overlay /// is on (then regardless of category), hidden otherwise. -pub fn categoryVisible(cat: ?i64, class: []const u8, symbol_name: ?[]const u8, m: *const MarinerSettings) bool { +pub fn categoryVisible(cat: ?i64, class: []const u8, symbol_name: ?[]const u8, m: *const Settings) bool { if (std.mem.eql(u8, class, "M_QUAL")) return m.data_quality; + // Meta-object boundaries (mirrors mariner.commonChartFilters): hidden unless + // the meta-bounds inspection view is on; when on, category still applies. + if (!m.show_meta_bounds) { + for ([_][]const u8{ "M_NPUB", "M_NSYS", "M_COVR", "M_CSCL" }) |mc| { + if (std.mem.eql(u8, class, mc)) return false; + } + } var c = cat orelse 1; if (symbol_name) |sn| { if (std.mem.eql(u8, sn, "ISODGR01")) c = if (m.show_isolated_dangers_shallow) 1 else 0; @@ -117,11 +124,11 @@ pub fn categoryVisible(cat: ?i64, class: []const u8, symbol_name: ?[]const u8, m } /// 1:N scale denominator of the whole world in one 256px tile at z0 — the -/// constant the style's SCAMIN gate divides by (assets/style.zig SCAMIN_GATE). +/// constant the style's SCAMIN gate divides by (style/maplibre.zig SCAMIN_GATE). pub const DENOM_Z0 = 279541132.0; /// SCAMIN gate at a (fractional) display zoom — mirrors the style expression -/// `zoom >= log2(DENOM_Z0 / scamin)` (assets/style.zig SCAMIN_GATE). A feature +/// `zoom >= log2(DENOM_Z0 / scamin)` (style/maplibre.zig SCAMIN_GATE). A feature /// without SCAMIN (null) always shows. pub fn scaminVisible(scamin: ?i64, zoom: f64) bool { const s = scamin orelse return true; @@ -134,14 +141,14 @@ pub fn scaminVisible(scamin: ?i64, zoom: f64) bool { /// i.e. zoom > log2(DENOM_Z0 / oscl). `oscl` is the baked X2 gate denominator /// (bake_enc.overscaleGateDenom = cscl/OVERSCALE_FACTOR), so this fires from X2 and /// NEVER before 1x compilation scale. The style clause is a strict `>` (oscl > -/// DENOM); oscl 0 (unknown) never shows. Mirrors assets/style.zig writeOsclClause. +/// DENOM); oscl 0 (unknown) never shows. Mirrors style/maplibre.zig writeOsclClause. pub fn osclVisible(oscl: i64, zoom: f64) bool { if (oscl <= 0) return false; return zoom > std.math.log2(DENOM_Z0 / @as(f64, @floatFromInt(oscl))); } /// Viewing-group gate (S-52 §14.5) — the deny-list model of -/// chartstyle.MarinerSettings.viewing_groups_off (the host's viewingGroupsOff model): +/// mariner.Settings.viewing_groups_off (the host's viewingGroupsOff model): /// a feature with no viewing group (vg 0) always shows; otherwise it hides iff /// its group is in the mariner's off-list. Any group not listed defaults ON. pub fn viewingGroupVisible(vg: i64, off: ?[]const i32) bool { @@ -153,10 +160,10 @@ pub fn viewingGroupVisible(vg: i64, off: ?[]const i32) bool { return true; } -/// S-52 §14.5 text-group gate — mirrors chartstyle.textGroupFilter: important +/// S-52 §14.5 text-group gate — mirrors mariner.textGroupFilter: important /// text (group 11) is always on; 21/26/29 ride text_names; 23 rides /// show_light_descriptions; everything else rides text_other. -pub fn textGroupVisible(group: i64, m: *const MarinerSettings) bool { +pub fn textGroupVisible(group: i64, m: *const Settings) bool { if (group == 11) return true; if (group == 21 or group == 26 or group == 29) return m.text_names; if (group == 23) return m.show_light_descriptions; @@ -166,7 +173,7 @@ pub fn textGroupVisible(group: i64, m: *const MarinerSettings) bool { /// Combined per-feature gate for pixel surfaces: display category + viewing /// group + SCAMIN at the scene zoom. `symbol_name` is the symbol about to be /// drawn (null for fills/lines/text) — only consulted for the ISODGR01 case. -pub fn visible(meta: *const rs.FeatureMeta, symbol_name: ?[]const u8, zoom: f64, m: *const MarinerSettings) bool { +pub fn visible(meta: *const rs.FeatureMeta, symbol_name: ?[]const u8, zoom: f64, m: *const Settings) bool { if (!categoryVisible(meta.cat, meta.class, symbol_name, m)) return false; if (!viewingGroupVisible(meta.vg, m.viewing_groups_off)) return false; if (!m.ignore_scamin and !scaminVisible(meta.scamin, zoom)) return false; @@ -178,7 +185,7 @@ pub fn visible(meta: *const rs.FeatureMeta, symbol_name: ?[]const u8, zoom: f64, if (!m.show_overscale or m.ignore_scamin) return false; if (!osclVisible(meta.oscl, zoom)) return false; } - // S-52 display-variant passes (mirrors chartstyle.boundaryFilter / + // S-52 display-variant passes (mirrors mariner.boundaryFilter / // pointStyleFilter): a feature portrayed twice carries bnd 1/0 (symbolized/ // plain boundary) or pts 0/1 (paper/simplified points); show the common // pass (2) + the mariner's active style — otherwise both passes double-draw. @@ -218,22 +225,22 @@ test "Colors: token -> RGB per palette, unknown -> null" { try std.testing.expectEqual(@as(?Rgb, null), c.get(.day, "NOSUCH")); } -test "categoryVisible mirrors chartstyle.categoryFilter" { - const def = MarinerSettings{}; // base+standard on, other off, no overlays +test "categoryVisible mirrors mariner.categoryFilter" { + const def = Settings{}; // base+standard on, other off, no overlays try std.testing.expect(categoryVisible(0, "DEPARE", null, &def)); try std.testing.expect(categoryVisible(1, "DEPARE", null, &def)); try std.testing.expect(!categoryVisible(2, "DEPARE", null, &def)); try std.testing.expect(categoryVisible(null, "DEPARE", null, &def)); // null -> standard // M_QUAL: data-quality overlay only. try std.testing.expect(!categoryVisible(0, "M_QUAL", null, &def)); - const dq = MarinerSettings{ .data_quality = true }; + const dq = Settings{ .data_quality = true }; try std.testing.expect(categoryVisible(2, "M_QUAL", null, &dq)); // shown regardless of cat // ISODGR01 rides its own toggle: off -> cat 0 (base on -> visible); // base ALSO off -> hidden; toggle on -> cat 1 (standard). try std.testing.expect(categoryVisible(2, "UWTROC", "ISODGR01", &def)); - const no_base = MarinerSettings{ .display_base = false }; + const no_base = Settings{ .display_base = false }; try std.testing.expect(!categoryVisible(2, "UWTROC", "ISODGR01", &no_base)); - const iso = MarinerSettings{ .display_base = false, .show_isolated_dangers_shallow = true }; + const iso = Settings{ .display_base = false, .show_isolated_dangers_shallow = true }; try std.testing.expect(categoryVisible(2, "UWTROC", "ISODGR01", &iso)); } @@ -245,7 +252,7 @@ test "scaminVisible mirrors the style SCAMIN_GATE" { try std.testing.expect(scaminVisible(0, 0)); // degenerate 0 -> always } -test "osclVisible: the X2 hatch never fires at/below 1x, fires past 2x (specs/overscale.md v3)" { +test "osclVisible: the X2 hatch never fires at/below 1x, fires past 2x" { // A 1:260000 cell bakes oscl = cscl / OVERSCALE_FACTOR (X2) = 130000 (see // bake_enc.overscaleGateDenom). The hatch (denom < oscl) must: // - stay OFF at & below 1x compilation scale (denom >= 260000), and @@ -267,15 +274,15 @@ test "osclVisible: the X2 hatch never fires at/below 1x, fires past 2x (specs/ov } test "visible: the overscale hatch honours show_overscale + the oscl gate" { - const m = MarinerSettings{}; + const m = Settings{}; // Baked X2 gate denom for a 1:260000 cell (cscl/OVERSCALE_FACTOR = 130000). // z_2x ~= log2(279541132/130000) ~= 11.07 — grossly overscale past there. const hatch = rs.FeatureMeta{ .cat = 0, .oscl = 130000, .overscale = true }; try std.testing.expect(!visible(&hatch, null, 10.5, &m)); // < 2x: no hatch try std.testing.expect(visible(&hatch, null, 12.0, &m)); // grossly overscale: hatch shows - const off = MarinerSettings{ .show_overscale = false }; + const off = Settings{ .show_overscale = false }; try std.testing.expect(!visible(&hatch, null, 12.0, &off)); - const ign = MarinerSettings{ .ignore_scamin = true }; + const ign = Settings{ .ignore_scamin = true }; try std.testing.expect(!visible(&hatch, null, 12.0, &ign)); // debug view: no hatch // An ordinary fill carrying the oscl TAG (not the hatch) is never oscl-gated. const fill = rs.FeatureMeta{ .cat = 0, .oscl = 130000 }; @@ -292,10 +299,10 @@ test "viewingGroupVisible: deny-list, vg 0 always shows" { } test "visible combines gates + honours ignore_scamin" { - const m = MarinerSettings{}; + const m = Settings{}; const meta = rs.FeatureMeta{ .cat = 1, .vg = 0, .scamin = 30000, .class = "BOYLAT" }; try std.testing.expect(!visible(&meta, "BOYLAT01", 12.0, &m)); // SCAMIN gates it try std.testing.expect(visible(&meta, "BOYLAT01", 14.0, &m)); - const ig = MarinerSettings{ .ignore_scamin = true }; + const ig = Settings{ .ignore_scamin = true }; try std.testing.expect(visible(&meta, "BOYLAT01", 12.0, &ig)); } diff --git a/src/render/surface.zig b/src/render/surface.zig index 0f03235..17b0f46 100644 --- a/src/render/surface.zig +++ b/src/render/surface.zig @@ -6,7 +6,7 @@ //! can serialize them for clients that re-style without re-baking; pixel //! surfaces resolve them through a shared lowering layer. //! -//! Rule: surfaces must NOT import s57, s100, or portray. If a surface needs a +//! Rule: surfaces must NOT import s57, s101, or portray. If a surface needs a //! fact the calls don't carry, that's an engine bug — extend the contract. //! //! Mirrors the original Go RenderSurface interface (internal/s52render). @@ -74,7 +74,7 @@ pub const TextStyle = struct { /// Per-feature S-52 metadata, bracketed around each feature's draw calls via /// beginFeature / endFeature. All pick data is pre-computed by the engine so -/// surfaces need not import s57/s100. +/// surfaces need not import s57/s101. pub const FeatureMeta = struct { draw_prio: i64 = 0, plane: i64 = 0, // S-101 DisplayPlane: 0 UnderRadar (default), 1 OverRadar @@ -99,6 +99,11 @@ pub const FeatureMeta = struct { // 0/1 = plain/symbolized boundary or paper/simplified point pass. bnd: i64 = 2, pts: i64 = 2, + // S-52 §8.6.2 suppressed boundary piece: geometry the producer masked as a + // cell-limit edge (MASK/USAG), baked anyway so the meta-bounds inspection + // view can outline meta objects; the standard display never shows it (the + // meta classes are filtered out entirely unless meta-bounds is on). + masked: bool = false, }; /// The render engine Surface vtable. diff --git a/src/render/vector.zig b/src/render/vector.zig index 25ec185..a56446b 100644 --- a/src/render/vector.zig +++ b/src/render/vector.zig @@ -140,7 +140,7 @@ pub const VectorSurface = struct { a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, - settings: *const resolve.MarinerSettings, + settings: *const resolve.Settings, cb: *const CSurface, store: ?sym.SymbolStore = null, fnt: ?fontmod.Font = null, @@ -150,6 +150,10 @@ pub const VectorSurface = struct { tz: u8 = 0, tx: u32 = 0, ty: u32 = 0, + // 1/2^tz and 1/(2^tz·EXTENT), set by setTile — worldOf runs per VERTEX, and + // recomputing pow(2,tz) there was ~9% of a whole view render. + inv_n: f64 = 1.0, + inv_ne: f64 = 1.0, cur: rs.FeatureMeta = .{}, cur_visible: bool = true, @@ -175,7 +179,7 @@ pub const VectorSurface = struct { .size_scale = sizeScale, }; - pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.MarinerSettings, cb: *const CSurface) VectorSurface { + pub fn init(a: Allocator, colors: *const resolve.Colors, palette: resolve.PaletteId, settings: *const resolve.Settings, cb: *const CSurface) VectorSurface { return .{ .a = a, .colors = colors, @@ -194,6 +198,8 @@ pub const VectorSurface = struct { self.tz = z; self.tx = x; self.ty = y; + self.inv_n = 1.0 / std.math.exp2(@as(f64, @floatFromInt(z))); + self.inv_ne = self.inv_n / @as(f64, @floatFromInt(tile.EXTENT)); } fn sp(ctx: *anyopaque) *VectorSurface { @@ -222,13 +228,12 @@ pub const VectorSurface = struct { return sp(ctx).refDev(); } - /// Tile-space point -> web-mercator world [0,1] (y down). + /// Tile-space point -> web-mercator world [0,1] (y down). Per-vertex hot + /// path: the tile factors are precomputed in setTile. 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, + .x = @as(f64, @floatFromInt(self.tx)) * self.inv_n + @as(f64, @floatFromInt(p.x)) * self.inv_ne, + .y = @as(f64, @floatFromInt(self.ty)) * self.inv_n + @as(f64, @floatFromInt(p.y)) * self.inv_ne, }; } @@ -334,7 +339,7 @@ pub const VectorSurface = struct { 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 + // show_inform_callouts (mariner.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; @@ -347,8 +352,7 @@ pub const VectorSurface = struct { // 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); + 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 @@ -418,8 +422,7 @@ pub const VectorSurface = struct { 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, self.refDev(), null) - else try self.emitSymbol(s, at, 0, sndfrm.SYMBOL_SCALE); + 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); } } diff --git a/src/root.zig b/src/root.zig index f427174..26e2820 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,11 +1,10 @@ -//! engine — the Zig chart tile generator. +//! The pure-Zig engine surface: the format, geometry, and encoding packages that +//! need no libc. It is the root of the unit-test build, which stays free of libc +//! and the embedded Lua so `zig build test` links no system C runtime. //! -//! Bottom-up build order (see ../../docs/docs/architecture.md): -//! M4: mvt + pmtiles + tile (encode MVT, write PMTiles) <- in progress -//! M5: capi (libtile57.a) for live in-process generation -//! M6: iso8211 + s57 decode -> embedded-Lua S-101 portrayal -//! -//! The Go project at ../../reference/chartplotter-go is the parity oracle. +//! The S-101 Lua portrayal runner lives in the separate `portray` module, added +//! on top of this surface by `bake_root.zig` (the CLI) and `lib_root.zig` +//! (libtile57.a). See build.zig for how the roots compose. const std = @import("std"); @@ -17,13 +16,13 @@ pub const tile = @import("tiles").tile; pub const iso8211 = @import("s57").iso8211; pub const s57 = @import("s57"); pub const scene = @import("scene"); -pub const s100 = @import("s100"); -pub const s101_instr = s100.s101_instr; -pub const s101_adapt = s100.s101_adapt; -pub const catalogue = s100.catalogue; +pub const s101 = @import("s101"); +pub const s101_instructions = s101.instructions; +pub const s101_adapter = s101.adapter; +pub const catalogue = s101.catalogue; pub const bake_enc = @import("scene").bake_enc; // banded multi-cell ENC_ROOT -> PMTiles -pub const assets = @import("assets"); // chart-bundle asset/manifest generation -pub const chartstyle = @import("assets").chartstyle; // mariner-driven MapLibre style patching +pub const style = @import("style"); // colortables, line styles, and style.json generation +pub const mariner = @import("style").mariner; // mariner-driven MapLibre style patching // capi (the C ABI) lives in lib_root.zig so the test/bake exes stay pure Zig. test { @@ -34,11 +33,11 @@ test { _ = iso8211; _ = s57; _ = scene; - _ = s101_instr; - _ = s101_adapt; + _ = s101_instructions; + _ = s101_adapter; _ = catalogue; _ = bake_enc; - _ = assets; - _ = chartstyle; + _ = style; + _ = mariner; _ = @import("mvt_parity_test.zig"); } diff --git a/src/s100/s100.zig b/src/s100/s100.zig deleted file mode 100644 index 6248526..0000000 --- a/src/s100/s100.zig +++ /dev/null @@ -1,15 +0,0 @@ -//! s100 — S-100 portrayal model. The distilled S-101 catalogue plus the -//! S-57 → S-101 adaptation and instruction layers. Pure Zig (no libc/Lua); -//! the Lua portrayal *runner* lives in engine's portray.zig. Mirrors the Go -//! oracle's `pkg/s100/*`. Built as a standalone Zig module; the embedded -//! catalogue/s57-codes JSON is attached in build.zig via addCatalogueJson. - -pub const catalogue = @import("catalogue.zig"); -pub const s101_adapt = @import("s101_adapt.zig"); -pub const s101_instr = @import("s101_instr.zig"); - -test { - _ = catalogue; - _ = s101_adapt; - _ = s101_instr; -} diff --git a/src/s100/s101_adapt.zig b/src/s101/adapter.zig similarity index 100% rename from src/s100/s101_adapt.zig rename to src/s101/adapter.zig diff --git a/src/s100/catalogue.zig b/src/s101/catalogue.zig similarity index 99% rename from src/s100/catalogue.zig rename to src/s101/catalogue.zig index ce5c564..cb61dfc 100644 --- a/src/s100/catalogue.zig +++ b/src/s101/catalogue.zig @@ -290,7 +290,7 @@ pub fn attrAcronym(attl: u16) ?[]const u8 { return c.attr_acronym.get(attl); } -// ---- Zig-side lookups (for s101_adapt) ----------------------------------- +// ---- Zig-side lookups (for the S-101 adapter) ---------------------------- pub fn resolveFeature(s57_acronym: []const u8) ?[]const u8 { ensureLoaded(); diff --git a/src/s100/s101_instr.zig b/src/s101/instructions.zig similarity index 100% rename from src/s100/s101_instr.zig rename to src/s101/instructions.zig diff --git a/src/s101/s101.zig b/src/s101/s101.zig new file mode 100644 index 0000000..8d5d2d0 --- /dev/null +++ b/src/s101/s101.zig @@ -0,0 +1,18 @@ +//! s101 — the S-101 portrayal model: the distilled S-101 catalogue plus the +//! S-57 -> S-101 adaptation and instruction layers. Pure Zig (no libc/Lua); the +//! Lua portrayal *runner* lives in the separate `portray` module. The embedded +//! catalogue / S-57-code JSON is attached in build.zig via addCatalogueJson. +//! +//! * catalogue — the distilled S-101 feature/attribute catalogue +//! * adapter — S-57 cell features -> S-101 feature/attribute records +//! * instructions — the portrayal instruction stream (points, lines, text) + +pub const catalogue = @import("catalogue.zig"); +pub const adapter = @import("adapter.zig"); +pub const instructions = @import("instructions.zig"); + +test { + _ = catalogue; + _ = adapter; + _ = instructions; +} diff --git a/src/s57/s57.zig b/src/s57/s57.zig index 9225911..ccd5fc6 100644 --- a/src/s57/s57.zig +++ b/src/s57/s57.zig @@ -10,9 +10,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -/// The ISO/IEC 8211 record parser S-57 rides on (folded into this module — -/// its only consumer). Re-exported for the CLI inspector + lib root. -pub const iso8211 = @import("iso8211.zig"); +/// The ISO/IEC 8211 container reader S-57 rides on, re-exported for consumers +/// that want the raw records. It is its own module (`@import("iso8211")`), so a +/// caller can depend on the 8211 layer without pulling in S-57 semantics. +pub const iso8211 = @import("iso8211"); const iso = iso8211; // Geographic coordinate stored in S-57's native integer ×1e7 units (lon ±1.8e9, @@ -750,7 +751,7 @@ pub const Cell = struct { /// lineGeometryParts (which only checks the current tail against the NEXT FSPT /// edge) fragments it into spurious parts. Byte-identical to lineGeometryParts /// when the FSPT list is already connected (the index's first-unused-touching - /// pick is then the next edge). The result feeds orientAreaRings, which derives + /// pick is then the next edge). The result feeds mvt.orientAreaRings, which derives /// exterior-vs-hole by geometric nesting, so ring USAG/order is not needed here. /// Lines keep the FSPT-order assembly (the oracle's constructLineStringGeometry /// is FSPT-order too). @@ -894,6 +895,63 @@ pub const Cell = struct { return parts.items; } + /// The complement of drawableLineParts: ONLY the edges §8.6.2 masking drops — + /// MASK==1, USAG==3, or coast-coincident on a non-coast-definer area. These are + /// the cell-limit stretches of a boundary; the meta-bounds inspection view bakes + /// them (tagged) so a meta object can be outlined even where its whole boundary + /// is the cell limit. Kept edges are chained into continuous parts like the + /// drawable walk; a drawn (or degenerate) edge breaks the chain. + pub fn maskedLineParts(self: Cell, a: Allocator, f: Feature) ![][]LonLat { + const mask_coast = f.prim == 3 and !isCoastDefiner(f.objl); + var parts = std.ArrayList([]LonLat).empty; + var cur = std.ArrayList(LonLat).empty; + var broken = false; + for (f.refs) |ref| { + if (ref.name.rcnm != RCNM_VE) continue; + const masked = ref.mask == 1 or ref.usag == 3 or (mask_coast and self.coast_edges.contains(ref.name.rcid)); + if (!masked) { + if (cur.items.len > 0) { + try parts.append(a, cur.items); + cur = std.ArrayList(LonLat).empty; + } + broken = true; // a drawn edge separates two masked stretches + continue; + } + const edge = try self.edgeCoordsRaw(a, ref.name.rcid, ref.ornt); + if (edge.len == 0) { + if (cur.items.len > 0) { + try parts.append(a, cur.items); + cur = std.ArrayList(LonLat).empty; + } + broken = true; + continue; + } + if (cur.items.len == 0 or broken) { + if (cur.items.len > 0) { + try parts.append(a, cur.items); + cur = std.ArrayList(LonLat).empty; + } + try cur.appendSlice(a, edge); + broken = false; + continue; + } + const tail = cur.items[cur.items.len - 1]; + const last = edge[edge.len - 1]; + if (tail.lon_e7 == edge[0].lon_e7 and tail.lat_e7 == edge[0].lat_e7) { + try cur.appendSlice(a, edge[1..]); + } else if (tail.lon_e7 == last.lon_e7 and tail.lat_e7 == last.lat_e7) { + std.mem.reverse(LonLat, edge); + try cur.appendSlice(a, edge[1..]); + } else { + try parts.append(a, cur.items); + cur = std.ArrayList(LonLat).empty; + try cur.appendSlice(a, edge); + } + } + if (cur.items.len > 0) try parts.append(a, cur.items); + return parts.items; + } + /// True when a feature's DRAWN boundary differs from its full geometry — so the /// caller must take the drawableLineParts subset instead of stroking the full /// ring. Either it carries explicit MASK/USAG edge flags, OR derived coast masking diff --git a/src/scene/bake_enc.zig b/src/scene/bake_enc.zig index 16c63f1..d1bdc61 100644 --- a/src/scene/bake_enc.zig +++ b/src/scene/bake_enc.zig @@ -22,8 +22,8 @@ const s57 = @import("s57"); const scene = @import("scene.zig"); const pmtiles = @import("tiles").pmtiles; const tile = @import("tiles").tile; -const assets = @import("assets"); // displayDenomZ: the physical display-scale formula -const geometry = @import("geo"); // Martinez boolean for the coverage-clipped composite +const style = @import("style"); // displayDenomZ: the physical display-scale formula +const geometry = @import("geometry"); // Martinez boolean for the coverage-clipped composite /// A parsed + portrayed cell ready to bake. `portrayal` is the per-feature S-101 /// instruction stream (null = bake with the classify() fallback); `bounds` is @@ -160,15 +160,13 @@ fn coversAny(backends: []const Backend, idxs: []const u32, lon: f64, lat: f64) b return coversAnyCtx(BackendCov{ .backends = backends }, idxs, lon, lat); } -// Whether cell `a` orders strictly before cell `b` in the equal-scale clip -// order: newer DSID issue/update date first (YYYYMMDD compares lexically; a -// dated cell orders before an undated one), then cell name ascending - total -// and deterministic for distinct cells, so bake output is byte-stable. +// The equal-scale clip-order tiebreak (newer date, then name) lives in +// geometry.partition, so the baker and the ownership partition pick the same +// winner. Re-exported here for this module's callers. +pub const ordersBeforeKeys = geometry.partition.ordersBeforeKeys; + fn ordersBefore(ctx: anytype, a_idx: u32, b_idx: u32) bool { - const da = ctx.date(a_idx); - const db = ctx.date(b_idx); - if (!std.mem.eql(u8, da, db)) return std.mem.lessThan(u8, db, da); // newer first - return std.mem.lessThan(u8, ctx.name(a_idx), ctx.name(b_idx)); + return ordersBeforeKeys(ctx.date(a_idx), ctx.name(a_idx), ctx.date(b_idx), ctx.name(b_idx)); } /// coverClipForCell's per-tile verdict — the cheap FULL/EMPTY/SEAM classifier @@ -218,9 +216,8 @@ fn ringsTouchRect(rings: []const []const s57.LonLat, w: f64, so: f64, e: f64, n: /// The finer-scale coverage to subtract from cell `cscl_self`'s fills in tile /// (z,x,y): the union of every strictly-finer cell's M_COVR (CATCOV=1), projected /// into the tile's i32 space and box-clipped, as a clean ring set. This is the -/// coverage-clipped best-available composite's exact subtrahend -/// (specs/cross-band-composition-redesign.md) — it replaces the point-sampled -/// whole-tile suppress_whole with true geometry, so a coarse fill is cut exactly +/// coverage-clipped best-available composite's exact subtrahend — it uses true +/// geometry rather than a point-sampled whole-tile flag, so a coarse fill is cut exactly /// where a finer cell owns the ground (no seam double-draw) and kept everywhere /// else, including inside a finer no-data hole (no blank). null = nothing finer /// covers this tile (the cell is finest here → draw everything). Allocated in `a` @@ -396,8 +393,8 @@ pub const OVERSCALE_FACTOR = 2; /// /// Baking the halved value (rather than the raw cscl quantized UP the SCAMIN /// ladder, the old behaviour) fixes the "fires early" defect: the zoom-derived -/// clause `oscl > K/2^zoom` now flips EXACTLY at 2x and never before 1x -/// (specs/overscale.md v3 defect 1). 0 when the compilation scale is unknown. +/// clause `oscl > K/2^zoom` flips EXACTLY at 2x and never before 1x. +/// Returns 0 when the compilation scale is unknown. pub fn overscaleGateDenom(cscl: i32) i64 { if (cscl <= 0) return 0; return @divTrunc(cscl, OVERSCALE_FACTOR); @@ -405,7 +402,7 @@ pub fn overscaleGateDenom(cscl: i32) i64 { /// The effScamin floor for a cell (spec §4): the display denominator of the /// cell's band-floor zoom under the client's static gate constant -/// (assets.scaminGateK at the equator — the largest K over all latitudes, so +/// (style.scaminGateK at the equator — the largest K over all latitudes, so /// the clamp holds everywhere). A feature's emitted `scamin` is floored here so /// an aggressive SCAMIN cannot blank a point between the cell's floor tier and /// its own activation zoom — the finer cell owns that ground geometrically, so @@ -420,12 +417,22 @@ pub fn effScaminFloor(cscl: i32) i64 { // window can open — leave its SCAMINs raw (a z0 clamp would disable // decluttering at world view entirely). if (floor_z == 0) return 0; - const k = assets.scaminGateK(0); + const k = style.scaminGateK(0); return @intFromFloat(@ceil(k / @as(f64, @floatFromInt(@as(u64, 1) << @intCast(floor_z))))); } -/// Native [minzoom, maxzoom] Web-Mercator span for a navigational-purpose band. -pub const ZoomRange = struct { min: u8, max: u8 }; +// The navigational-band / scale->zoom mapping lives in tiles.band (a pure leaf +// both the baker and the compositor share). Re-exported here so this module's +// callers (chart, coverage) keep reaching it through bake_enc. (Inline @import +// rather than a `band` binding, so the many `band: Band` parameters below don't +// shadow it.) +pub const ZoomRange = @import("tiles").band.ZoomRange; +pub const Band = @import("tiles").band.Band; +pub const bands_fine_to_coarse = @import("tiles").band.bands_fine_to_coarse; +pub const bandOf = @import("tiles").band.bandOf; +pub const FILLUP_DZ = @import("tiles").band.FILLUP_DZ; +pub const FILLUP_CEIL = @import("tiles").band.FILLUP_CEIL; +pub const bandZooms = @import("tiles").band.bandZooms; /// Restricts a bakeBand pass to the tiles under one "super-tile" (a tile at the /// coarser zoom `zs`): only (z,x,y) with z >= zs whose ancestor at zs is (sx,sy) @@ -433,55 +440,6 @@ pub const ZoomRange = struct { min: u8, max: u8 }; /// time — loading only the cells overlapping it — instead of the whole band. pub const TileClip = struct { zs: u8, sx: u32, sy: u32 }; -/// Navigational-purpose bands, finest → coarsest (the order bands must be baked in -/// for best-band dedup). Mirrors chartplotter-go bake.Band. -pub const Band = enum(u8) { berthing = 0, harbor, approach, coastal, general, overview }; - -/// All bands finest → coarsest (the bakeBand call order). -pub const bands_fine_to_coarse = [_]Band{ .berthing, .harbor, .approach, .coastal, .general, .overview }; - -/// Map a compilation-scale denominator (CSCL, 1:N) to its band (Go BandForScale). -pub fn bandOf(cscl: i32) Band { - const n: i64 = if (cscl <= 0) 50_000 else cscl; - if (n <= 8_000) return .berthing; - if (n <= 32_000) return .harbor; - if (n <= 130_000) return .approach; - if (n <= 500_000) return .coastal; - if (n <= 2_300_000) return .general; - return .overview; -} - -/// Overscale fill-up depth DEFAULT: how many zooms past its native max a -/// band's own cells keep baking (only where nothing finer already emitted). -/// Every extension zoom ~4x that band's tile count over its uncovered -/// footprint (measured: +2 turned a 5.6k-tile approach pass into 41k), so the -/// default is ONE crisp overscale zoom; TILE57_FILLUP_DZ=0..2 overrides per -/// bake (Baker.fillup_dz). 0 never blanks — the client camera stops at the -/// probed data depth and MapLibre stretches one level past it. -pub const FILLUP_DZ: u8 = 1; - -/// Absolute fill-up ceiling: extension zooms never exceed this. The fill-up -/// serves the MID-ZOOM seam where a coarse chart is the finest coverage (the -/// blank bay at z12-15); letting fine bands extend too (harbor→z17-18, -/// berthing→z19-20) quadruples the tile count per extra zoom across every -/// harbor footprint for content nobody needs — a district pack ballooned from -/// ~800k to 13M+ planned tiles. A band's NATIVE window is never clamped by -/// this; past its data the camera stops at the probed depth instead. -pub const FILLUP_CEIL: u8 = 15; - -/// A band's native zoom span (Go Band.ZoomRange). Adjacent bands overlap by one -/// zoom; best-band dedup resolves the overlap to the finer band. -pub fn bandZooms(band: Band) ZoomRange { - return switch (band) { - .berthing => .{ .min = 16, .max = 18 }, - .harbor => .{ .min = 13, .max = 16 }, - .approach => .{ .min = 11, .max = 13 }, - .coastal => .{ .min = 9, .max = 11 }, - .general => .{ .min = 7, .max = 9 }, - .overview => .{ .min = 0, .max = 7 }, - }; -} - /// The live-path fallback band for a tile whose zoom sits OUTSIDE every /// overlapping cell's native window (chart.zig tileRefs). ABOVE every window /// (z > the finest band's max — band maxima are monotonic in fineness, so the @@ -603,6 +561,16 @@ fn parWorker(pc: *ParCtx) void { } } +/// Run func(user, i, scratch) for every i in [0, n) SERIALLY on the calling thread (a per-thread +/// scratch arena reset between items, exactly like parallelFor with one thread). Tile generation +/// uses this: the parallel unit is the CELL (chart.bakeCellsParallel runs one worker per cell), so +/// per-cell tile-gen stays single-threaded and W workers stay W threads — no nested thread pool. +pub fn serialFor(gpa: std.mem.Allocator, n: usize, user: *anyopaque, func: *const fn (*anyopaque, usize, std.mem.Allocator) void) void { + if (n == 0) return; + var pc = ParCtx{ .next = std.atomic.Value(usize).init(0), .n = n, .user = user, .func = func, .gpa = gpa }; + parWorker(&pc); +} + /// Run func(user, i, scratch) for every i in [0, n) across the CPU threads. func /// must be safe to call concurrently for distinct i (no shared mutable state). /// `scratch` is a per-thread arena reset between items — use it for transient @@ -735,7 +703,7 @@ const TileGenCtx = struct { // composite (coverClipForCell + subtractCoverage + coveredByFiner) does it // exactly per-feature. All that remains here is the overscale scale-boundary // scan: the finest compilation scale CONTRIBUTING to this tile. - // Scale-boundary overscale refinement (specs/overscale.md): the finest + // Scale-boundary overscale refinement: the finest // compilation scale CONTRIBUTING to this tile — over the quilting's own // participant list (any overlapping cell, reach-only riders excluded), // not point-sampled coverage. A cell hatches its OVERSC01 coverage only @@ -1098,7 +1066,7 @@ pub const Baker = struct { const bname: [*:0]const u8 = @tagName(band).ptr; var done = std.atomic.Value(usize).init(0); var tg = TileGenCtx{ .keys = keys, .idx_lists = idx_lists, .results = results, .backends = backends, .gpa = self.gpa, .format = self.format, .pick_attrs = self.pick_attrs, .context = self.context, .progress = progress, .pctx = ctx, .base = self.count, .band_base = self.band_base, .band_total = self.band_total, .band_index = self.band_index, .band_count = self.band_count, .band_name = bname, .done = &done }; - parallelFor(self.gpa, n, &tg, TileGenCtx.gen); + serialFor(self.gpa, n, &tg, TileGenCtx.gen); for (keys, results) |key, mvt_opt| { const mvt_bytes = mvt_opt orelse continue; @@ -1179,7 +1147,7 @@ pub const Baker = struct { const bname: [*:0]const u8 = @tagName(band).ptr; var done = std.atomic.Value(usize).init(0); var tg = TileGenCtx{ .keys = keys, .idx_lists = idx_lists, .results = results, .backends = backends, .gpa = self.gpa, .format = self.format, .pick_attrs = self.pick_attrs, .context = self.context, .progress = progress, .pctx = ctx, .base = self.count, .band_base = self.band_base, .band_total = self.band_total, .band_index = self.band_index, .band_count = self.band_count, .band_name = bname, .done = &done }; - parallelFor(self.gpa, n, &tg, TileGenCtx.gen); + serialFor(self.gpa, n, &tg, TileGenCtx.gen); for (keys, results) |key, mvt_opt| { const mvt_bytes = mvt_opt orelse continue; @@ -1522,15 +1490,26 @@ test "fill-down bake: a coastal-only bay gets low-zoom tiles the overview extend // band the extend_min fill rides), but it does not cover the bay. Without // fill-down the bay has no tiles below coastal's window (z<9): the reported // district-pack z6–z8 hole. + // Two features: a SCAMIN-gated buoy (hidden below its display scale — the + // sub-band cull drops it from fill-down tiles, where the client gate would + // hide it anyway) and a SCAMIN-less one (land/coast in real cells) that + // keeps the low-zoom tiles populated. const scamin_attr = [_]s57.Attr{.{ .code = 133, .value = "260000" }}; - const feats = [_]s57.Feature{.{ + const feats = [_]s57.Feature{ .{ .rcnm = 0, .rcid = 1, .prim = 1, .objl = 14, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 1 }, .ornt = 255 }}, .attrs = &scamin_attr, - }}; + }, .{ + .rcnm = 0, + .rcid = 2, + .prim = 1, + .objl = 14, + .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 1 }, .ornt = 255 }}, + .attrs = &.{}, + } }; var bay_cell = try testCell(gpa, 0.35, 0.35, 200_000, &feats); defer bay_cell.deinit(); var ov_cell = try testCell(gpa, 10.5, 10.5, 3_000_000, &feats); @@ -1545,7 +1524,7 @@ test "fill-down bake: a coastal-only bay gets low-zoom tiles the overview extend const cover = [_][]const []const s57.LonLat{&rings}; const bay_bounds = [4]f64{ 0.2, 0.2, 0.5, 0.5 }; const ov_bounds = [4]f64{ 10.2, 10.2, 10.8, 10.8 }; - const streams = [_]?[]const u8{"DrawingPriority:7;PointInstruction:BOYLAT01"}; + const streams = [_]?[]const u8{ "DrawingPriority:7;PointInstruction:BOYLAT01", "DrawingPriority:7;PointInstruction:BOYLAT01" }; const scamins = [_]u32{260_000}; var bay = Backend{ .cell = bay_cell, .portrayal = &streams, .bounds = bay_bounds, .cscl = 200_000, .coverage = &cover, .scamins = &scamins }; @@ -1574,7 +1553,7 @@ test "fill-down bake: a coastal-only bay gets low-zoom tiles the overview extend try std.testing.expect(ov_t6[0] != bay_t6[0] or ov_t6[1] != bay_t6[1]); } -test "overscale: a coarse cell occluded everywhere by finer coverage emits NO hatch (specs/overscale.md v3 defect 2)" { +test "overscale: a coarse cell occluded everywhere by finer coverage emits NO hatch" { const gpa = std.testing.allocator; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); diff --git a/src/scene/lightreach.zig b/src/scene/lightreach.zig new file mode 100644 index 0000000..9bbb96a --- /dev/null +++ b/src/scene/lightreach.zig @@ -0,0 +1,144 @@ +//! Light-sector reach: how far a LIGHTS feature's sector legs/arcs reach, so the +//! baker and the emitter widen the LIGHTS spatial-cull margin enough that a +//! directional light's ground legs are not dropped on the tiles they cross. +//! Pure over s57. + +const std = @import("std"); +const s57 = @import("s57"); + +// The reach bounds live in the tiles leaf (tiles.tile) so the runtime +// compositor — a dependency leaf without the scene — applies the SAME ring +// when it pulls a cell's figures into neighbouring tiles. Re-exported here +// for the baker/emitter callers. GROUND-length legs (directional lights: +// nmi2metres(nominal range), LightSectored.lua) exceed the mm bound by far +// at fine zooms — lightReachTiles is the honest per-zoom bound. +pub const LIGHT_AUG_REACH_TILES = @import("tiles").tile.LIGHT_AUG_REACH_TILES; +pub const lightReachTiles = @import("tiles").tile.lightReachTiles; + +/// How far a cell's constructed sector figures (emitAugFigures) can reach beyond +/// their feature anchors, summarised per cell so tile addressing (bake_enc +/// buildTileMap / the live tileRefs) can include the cell in neighbouring tiles +/// its raw bbox never touches — otherwise legs/arcs clip exactly at the boundary. +pub const LightReach = struct { + /// Union bbox [w,s,e,n] of the aug-figure-bearing features' anchors (feature + /// nodes + explicit AugmentedPoint anchors); null = no sector figures at all. + bbox: ?[4]f64 = null, + /// Max ground-distance leg length in metres (AugmentedRay with a GeographicCRS + /// length — directional-light legs); 0 = display-mm figures only. + range_m: f64 = 0, +}; + +// The nth comma-separated field of an instruction argument ("" past the end). +fn instrCsv(s: []const u8, n: usize) []const u8 { + var it = std.mem.splitScalar(u8, s, ','); + var i: usize = 0; + while (it.next()) |part| : (i += 1) if (i == n) return std.mem.trim(u8, part, " "); + return ""; +} + +// Fold one aug-figure-bearing feature's anchors + ground length into `r`: +// the feature node plus any explicit AugmentedPoint anchors in the stream. +fn foldLightReach(r: *LightReach, cell: *const s57.Cell, f: s57.Feature, stream: []const u8) void { + var b: [4]f64 = if (r.bbox) |bb| bb else .{ 1e9, 1e9, -1e9, -1e9 }; + if (cell.pointGeometry(f)) |pg| { + b[0] = @min(b[0], pg.lon()); + b[1] = @min(b[1], pg.lat()); + b[2] = @max(b[2], pg.lon()); + b[3] = @max(b[3], pg.lat()); + } + var it = std.mem.splitScalar(u8, stream, ';'); + while (it.next()) |item| { + const colon = std.mem.indexOfScalar(u8, item, ':') orelse continue; + const key = item[0..colon]; + const val = item[colon + 1 ..]; + if (std.mem.eql(u8, key, "AugmentedRay")) { + // "AugmentedRay:,,," — a GeographicCRS + // length is ground metres (the directional-light leg); else display mm. + if (std.mem.eql(u8, instrCsv(val, 2), "GeographicCRS")) { + const len = std.fmt.parseFloat(f64, instrCsv(val, 3)) catch 0; + r.range_m = @max(r.range_m, len); + } + } else if (std.mem.eql(u8, key, "AugmentedPoint")) { + // "AugmentedPoint:,," — an explicit figure anchor. + const lon = std.fmt.parseFloat(f64, instrCsv(val, 1)) catch continue; + const lat = std.fmt.parseFloat(f64, instrCsv(val, 2)) catch continue; + b[0] = @min(b[0], lon); + b[1] = @min(b[1], lat); + b[2] = @max(b[2], lon); + b[3] = @max(b[3], lat); + } + } + if (b[0] <= b[2]) r.bbox = b; +} + +/// EXACT per-cell sector-figure reach, from the portrayal instruction streams: +/// a feature constructs figures iff its stream carries AugmentedRay/ArcByRadius +/// (LightSectored legs/arcs), so this can't drift from what emitAugFigures will +/// actually draw (including context-parameter effects like FullLightLines). +/// Only prim==1 features can emit figures (processFeatureParsed). Allocation-free. +pub fn collectLightReach(cell: *const s57.Cell, portrayal: ?[]const ?[]const u8) LightReach { + var r = LightReach{}; + const streams = portrayal orelse return r; + for (cell.features, 0..) |f, fi| { + if (f.prim != 1 or fi >= streams.len) continue; + const stream = streams[fi] orelse continue; + if (std.mem.indexOf(u8, stream, "AugmentedRay:") == null and + std.mem.indexOf(u8, stream, "ArcByRadius:") == null) continue; + foldLightReach(&r, cell, f, stream); + } + return r; +} + +test "collectLightReach: AugmentedRay ground legs + anchors from the streams" { + const gpa = std.testing.allocator; + const feats = [_]s57.Feature{ + // A directional light: GeographicCRS leg 16668 m + a sector arc. + .{ .rcnm = 100, .rcid = 1, .prim = 1, .objl = 75, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 1 }, .ornt = 255 }} }, + // A plain sectored light: LocalCRS (display-mm) legs only. + .{ .rcnm = 100, .rcid = 2, .prim = 1, .objl = 75, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 2 }, .ornt = 255 }} }, + // A buoy with no figures at all. + .{ .rcnm = 100, .rcid = 3, .prim = 1, .objl = 14, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 3 }, .ornt = 255 }} }, + }; + var cell = s57.Cell{ + .params = .{ .cscl = 80_000 }, + .vectors = &.{}, + .features = &feats, + .nodes = std.AutoHashMap(u64, s57.LonLat).init(gpa), + .edges = std.AutoHashMap(u32, usize).init(gpa), + .sounding_vecs = std.AutoHashMap(u64, usize).init(gpa), + .arena = std.heap.ArenaAllocator.init(gpa), + }; + defer cell.deinit(); + try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 1, s57.LonLat.init(-76.52, 39.20)); + try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 2, s57.LonLat.init(-76.40, 39.30)); + try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 3, s57.LonLat.init(-76.10, 39.10)); + + const streams = [_]?[]const u8{ + "ViewingGroup:27070;DrawingPriority:8;AugmentedRay:GeographicCRS,45.0,GeographicCRS,16668;LineStyle:dash,3.51,0.32,CHBLK;LineInstruction:_simple_;ClearGeometry", + "DrawingPriority:8;AugmentedRay:GeographicCRS,120.0,LocalCRS,25.0;LineInstruction:_simple_;ArcByRadius:0,0,20,120,90;LineInstruction:_simple_;ClearGeometry", + "DrawingPriority:7;PointInstruction:BOYLAT01", + }; + const r = collectLightReach(&cell, &streams); + // Ground range from the directional leg only; mm-only figures add no range. + try std.testing.expectEqual(@as(f64, 16668), r.range_m); + // The bbox unions BOTH figure-bearing lights, not the figuresless buoy. + const bb = r.bbox orelse return error.TestUnexpectedResult; + try std.testing.expectApproxEqAbs(@as(f64, -76.52), bb[0], 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, 39.20), bb[1], 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, -76.40), bb[2], 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, 39.30), bb[3], 1e-9); + + // No streams at all -> no reach. + try std.testing.expectEqual(@as(?[4]f64, null), collectLightReach(&cell, null).bbox); + + // An explicit AugmentedPoint anchor extends the bbox beyond the node. + const anchored = [_]?[]const u8{ + "AugmentedPoint:GeographicCRS,-76.60,39.10;ArcByRadius:0,0,25,0,360;LineInstruction:_simple_;ClearGeometry", + null, + null, + }; + const ra = collectLightReach(&cell, &anchored); + const ab = ra.bbox orelse return error.TestUnexpectedResult; + try std.testing.expectApproxEqAbs(@as(f64, -76.60), ab[0], 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, 39.10), ab[1], 1e-9); +} diff --git a/src/scene/linestyle.zig b/src/scene/linestyle.zig new file mode 100644 index 0000000..f684c36 --- /dev/null +++ b/src/scene/linestyle.zig @@ -0,0 +1,230 @@ +//! Complex (symbolised) line tessellation (S-101 LineStyles): a named linestyle +//! (LC, or a LineInstruction whose style is not "_simple_") is walked by arc length +//! and emitted per period as dash "on" runs plus each embedded symbol as a point +//! rotated to the local tangent. The registry (id -> Info) is set once by the +//! baker before tile generation, then read-only during the parallel bake. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const s57 = @import("s57"); +const tile = @import("tiles").tile; +const mvt = @import("tiles").mvt; +const style = @import("style"); +const rs = @import("render").surface; +const SYMBOL_SCALE: f64 = @import("render").sndfrm.SYMBOL_SCALE; + +// === Complex (symbolised) line tessellation (S-101 LineStyles) ============= +// A named linestyle (LC / a LineInstruction whose style is not "_simple_") is +// tessellated per zoom: walk the line by arc length and emit, per period, the dash +// "on" runs as line segments + each embedded symbol as a point rotated to the local +// tangent. Mirrors Go bake/complexline.go + linestyle_catalog.go. The mm geometry is +// parsed by style.parseLineStyle; the baker converts it to Info at the PresLib +// FEATURE scale (ls_px_per_mm) and registers it before baking. + +const ls_feature_scale: f64 = 0.01 / 0.35278; // px per 0.01-mm PresLib unit (= SYMBOL_SCALE) +const ls_px_per_mm: f64 = 100.0 * ls_feature_scale; // mm -> screen px +/// The mm->px feature scale the baker must apply when building an Info from the raw +/// millimetre LineStyles geometry (style.parseLineStyle), so the tessellator and the +/// table agree. Differs from the symbol-scale style.analysePattern uses for the +/// client linestyles.json. +pub const LINESTYLE_PX_PER_MM = ls_px_per_mm; + +pub const Symbol = struct { name: []const u8, offset_px: f64 }; +pub const Info = struct { + period_px: f64, + on_runs: []const [2]f64, // [lo,hi] screen px from period start + symbols: []const Symbol, + color_token: []const u8, + width_px: f64, +}; + +// Set once by the baker before tile generation; read-only during the parallel bake +// (encodeTile only reads), so it needs no lock. Absent => named lines fall +// back to the generic dashed stroke (live/host path, no regression). +var g_linestyles: std.StringHashMapUnmanaged(Info) = .{}; + +/// Register one analysed complex linestyle (id = LineStyles file stem). `id` and the +/// Info slices must outlive the bake (embedded XML / the bake's long-lived alloc). +pub fn registerLinestyle(gpa: Allocator, id: []const u8, info: Info) void { + g_linestyles.put(gpa, id, info) catch {}; +} + +/// Look up a registered complex linestyle by id (LineStyles file stem); null if +/// unregistered, so the caller falls back to a generic dashed stroke. +pub fn lookup(id: []const u8) ?Info { + return g_linestyles.get(id); +} + +/// Populate the complex-linestyle table from S-101 LineStyles XML sources +/// (id = file stem). IDEMPOTENT — a populated table is left untouched, so +/// every scene entry point (bake, lib renderView, CLI render) can call it +/// unconditionally; forgetting it silently degrades named linestyles +/// (MARSYS51, cables, pipelines, …) to generic dashed strokes. Mirrors the +/// Go lsInfoFromCatalog: mm geometry at the PresLib feature scale, S-52 +/// minimum pen width. `gpa` + `srcs` must outlive all tile generation. +pub fn registerLinestylesXml(gpa: Allocator, srcs: []const style.LineStyleSrc) void { + if (g_linestyles.count() > 0) return; + const px = LINESTYLE_PX_PER_MM; + for (srcs) |s| { + const parsed = style.parseLineStyle(gpa, s.xml) catch continue; + const period = parsed.interval_length * px; + if (period < 0.5) continue; // no interval to tile (pure-symbol style) + var runs = std.ArrayList([2]f64).empty; + for (parsed.dashes) |d| { + const lo = d.start * px; + const hi = (d.start + d.length) * px; + if (hi - lo > 1e-6) runs.append(gpa, .{ lo, hi }) catch {}; + } + var syms = std.ArrayList(Symbol).empty; + for (parsed.symbols) |sym| syms.append(gpa, .{ .name = sym.reference, .offset_px = sym.position * px }) catch {}; + var width = parsed.pen_width * px; + if (width < 0.6) width = 0.9; // S-52 minimum pen + registerLinestyle(gpa, s.id, .{ + .period_px = period, + .on_runs = runs.items, + .symbols = syms.items, + .color_token = parsed.pen_color, + .width_px = width, + }); + } +} + +const LsTangent = struct { p: tile.FPoint, dx: f64, dy: f64 }; + +/// Point at local arc `d` along rp plus the (un-normalised) tangent of its segment. +fn lsPointAndTangent(rp: []const tile.FPoint, rarc: []const f64, d_in: f64) ?LsTangent { + const total = rarc[rarc.len - 1]; + const d = std.math.clamp(d_in, 0, total); + var i: usize = 0; + while (i + 1 < rp.len) : (i += 1) { + if (d <= rarc[i + 1] or i + 2 == rp.len) { + const seg = rarc[i + 1] - rarc[i]; + const t: f64 = if (seg > 1e-12) (d - rarc[i]) / seg else 0; + return .{ + .p = .{ .x = rp[i].x + t * (rp[i + 1].x - rp[i].x), .y = rp[i].y + t * (rp[i + 1].y - rp[i].y) }, + .dx = rp[i + 1].x - rp[i].x, + .dy = rp[i + 1].y - rp[i].y, + }; + } + } + return null; +} + +fn lsLerpArc(rp: []const tile.FPoint, rarc: []const f64, d: f64) tile.FPoint { + return (lsPointAndTangent(rp, rarc, d) orelse LsTangent{ .p = rp[0], .dx = 0, .dy = 0 }).p; +} + +/// Sub-polyline of rp between local arc distances d0..d1 (endpoints interpolated). +fn lsSubPathByArc(a: Allocator, rp: []const tile.FPoint, rarc: []const f64, d0_in: f64, d1_in: f64) ![]tile.FPoint { + const total = rarc[rarc.len - 1]; + const d0 = std.math.clamp(d0_in, 0, total); + const d1 = std.math.clamp(d1_in, 0, total); + if (d1 - d0 < 1e-9) return &.{}; + var out = std.ArrayList(tile.FPoint).empty; + try out.append(a, lsLerpArc(rp, rarc, d0)); + for (rp, 0..) |p, i| { + if (rarc[i] > d0 and rarc[i] < d1) try out.append(a, p); + } + try out.append(a, lsLerpArc(rp, rarc, d1)); + return out.items; +} + +/// 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. +/// 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. +pub fn drawComplexRun(a: Allocator, rp: []const tile.FPoint, arc0: f64, info: Info, 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 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. +pub fn drawComplexLine(a: Allocator, parts: []const []s57.LonLat, info: Info, line_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; + const fpts = try a.alloc(tile.FPoint, part.len); + for (part, 0..) |pt, i| fpts[i] = tile.worldToTileF(tile.lonLatToWorld(pt.lon(), pt.lat()), z, x, y, tile.EXTENT); + const arc = try a.alloc(f64, part.len); + 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| { + 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(line_style, color, info.width_px, run.arc0, qpts); + } else { + // LIVE / CLI render: walk the period at this surface's display scale. + try drawComplexRun(a, run.points, run.arc0, info, color, ss, emit_symbols, surf); + } + } + } + try surf.endFeature(); +} + +/// Draw a plain (solid or dashed) line: clip + simplify each projected run for +/// this tile and stroke it. The symbolised-line counterpart is drawComplexLine. +pub fn drawPlainLine(a: Allocator, stroke_proj: []const []const mvt.Point, color: []const u8, width: f64, dash: rs.Dash, box: tile.Box, fmeta: *const rs.FeatureMeta, valdco: ?f64, surf: rs.Surface) !void { + for (stroke_proj) |proj| { + const sub = try tile.clipSimplifyLine(a, proj, box); + if (sub.len == 0) continue; + try surf.beginFeature(fmeta); + try surf.strokeLine(color, width, dash, sub, valdco); + try surf.endFeature(); + } +} diff --git a/src/scene/replay.zig b/src/scene/replay.zig new file mode 100644 index 0000000..4b7c376 --- /dev/null +++ b/src/scene/replay.zig @@ -0,0 +1,166 @@ +//! Bundle-sourced replay: turn a decoded baked tile (mvt layers) back into +//! Surface draw calls, so the native pixel/PDF/ASCII path renders a pre-baked +//! archive the same way it renders a live cell. Reads the tile properties the +//! scene emitter wrote; re-walks complex linestyles at the surface's display +//! scale through the linestyle module. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const mvt = @import("tiles").mvt; +const tile = @import("tiles").tile; +const rs = @import("render").surface; +const linestyle = @import("linestyle.zig"); +const SYMBOL_SCALE: f64 = @import("render").sndfrm.SYMBOL_SCALE; + +// +// The tile schema is a serialized Surface-call stream, so a baked tile can be +// replayed onto any Surface — the substrate for rendering PMTiles bundles to +// pixels with no source cells. LOSSY by design: the bake-time portrayal +// context is frozen (SafetyContour/Depth 30); the live-swappable props the +// tile path bakes (danger_depth/sym_deep, sym_s/sym_g depth + quality-ring +// prefixes) are re-expanded here, so the mariner's danger swap, sounding +// bold/faint split, and display unit still evaluate LIVE. + +fn propOf(props: []const mvt.Prop, key: []const u8) ?mvt.Value { + for (props) |p| if (std.mem.eql(u8, p.key, key)) return p.value; + return null; +} + +fn propInt(props: []const mvt.Prop, key: []const u8, default: i64) i64 { + const v = propOf(props, key) orelse return default; + return switch (v) { + .int => |i| i, + .uint => |u| @intCast(u), + .double => |d| @intFromFloat(d), + .float => |f| @intFromFloat(f), + else => default, + }; +} + +fn propF64(props: []const mvt.Prop, key: []const u8) ?f64 { + const v = propOf(props, key) orelse return null; + return switch (v) { + .double => |d| d, + .float => |f| f, + .int => |i| @floatFromInt(i), + .uint => |u| @floatFromInt(u), + else => null, + }; +} + +fn propStr(props: []const mvt.Prop, key: []const u8) []const u8 { + const v = propOf(props, key) orelse return ""; + return switch (v) { + .string => |s| s, + else => "", + }; +} + +fn metaFromProps(props: []const mvt.Prop) rs.FeatureMeta { + return .{ + .draw_prio = propInt(props, "draw_prio", 0), + .cat = propInt(props, "cat", 1), + .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), + .masked = propInt(props, "masked", 0) != 0, + .date_start = propStr(props, "date_start"), + .date_end = propStr(props, "date_end"), + }; +} + +/// 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(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"); + const is_lines = std.mem.startsWith(u8, layer.name, "lines"); + const is_points = std.mem.startsWith(u8, layer.name, "point_symbols"); + const is_soundings = std.mem.eql(u8, layer.name, "soundings"); + const is_text = std.mem.startsWith(u8, layer.name, "text"); + for (layer.features) |f| { + const meta = metaFromProps(f.properties); + try surf.beginFeature(&meta); + defer surf.endFeature() catch {}; + if (is_patterns) { + try surf.fillPattern(propStr(f.properties, "pattern_name"), f.parts); + } else if (is_areas) { + const d1 = propF64(f.properties, "drval1"); + 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 (linestyle.lookup(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 linestyle.drawComplexRun(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) { + if (f.parts.len == 0 or f.parts[0].len == 0) continue; + try surf.drawSymbol( + propStr(f.properties, "symbol_name"), + f.parts[0][0], + propF64(f.properties, "rotation_deg") orelse 0, + propF64(f.properties, "scale") orelse SYMBOL_SCALE, + propInt(f.properties, "rot_north", 0) != 0, + .point, + propF64(f.properties, "danger_depth"), // live swap re-evaluates + ); + } else if (is_soundings) { + if (f.parts.len == 0 or f.parts[0].len == 0) continue; + const depth = propF64(f.properties, "depth") orelse continue; + // The quality-ring flags are encoded in the baked glyph list's + // leading tokens (SNDFRM04 B1 swept / C2/C3 low-accuracy) — + // recover them so the live recomposition keeps the rings. + const sym_s = propStr(f.properties, "sym_s"); + const swept = std.mem.indexOf(u8, sym_s, "SB1") != null; + const low_acc = std.mem.indexOf(u8, sym_s, "SC3") != null; + try surf.drawSounding(depth, swept, low_acc, f.parts[0][0]); + } else if (is_text) { + if (f.parts.len == 0 or f.parts[0].len == 0) continue; + var ox: f64 = 0; + var oy: f64 = 0; + const loff = propStr(f.properties, "loff"); + if (loff.len > 0) { + var it = std.mem.splitScalar(u8, loff, ','); + const TEXT_BODY_MM = 3.51; + ox = (std.fmt.parseFloat(f64, it.next() orelse "0") catch 0) * TEXT_BODY_MM; + oy = (std.fmt.parseFloat(f64, it.next() orelse "0") catch 0) * TEXT_BODY_MM; + } + const ts = rs.TextStyle{ + .color = propStr(f.properties, "color_token"), + .font_size = propF64(f.properties, "font_size_px") orelse 12, + .halign = propStr(f.properties, "halign"), + .valign = propStr(f.properties, "valign"), + .offset_x = ox, + .offset_y = oy, + .group = propInt(f.properties, "tgrp", 0), + }; + try surf.drawText(propStr(f.properties, "text"), &ts, f.parts[0][0]); + } + } + } +} diff --git a/src/scene/scene.zig b/src/scene/scene.zig index d315215..fc33913 100644 --- a/src/scene/scene.zig +++ b/src/scene/scene.zig @@ -1,9 +1,14 @@ -//! Direct S-57 -> MVT tile generation (M6c demo, BYPASSING S-101 portrayal). +//! The tile engine: S-57 features plus their S-101 portrayal instructions become +//! a tile Surface, then an MVT or MLT vector tile for a given (z, x, y). //! -//! Generates a vector tile for (z,x,y) straight from an S-57 cell with a small -//! hardcoded object-class -> S-52 color-token mapping, so the existing chart -//! style renders it. This proves cell -> MVT -> MapLibre end to end before the -//! S-101 Lua portrayal engine lands and replaces classify() with real rules. +//! A cell's features carry S-101 portrayal instructions from the Lua rules (see +//! the `portray` module). This module projects each feature's geometry to tile +//! space, resolves its layer and draw properties, and serializes the result. A +//! `classify()` fallback supplies a small object-class -> S-52 mapping for the +//! few paths that run without portrayal instructions. +//! +//! This module also hosts the banded multi-cell ENC_ROOT baker (`bake_enc`), +//! the engine's batch driver. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -12,22 +17,37 @@ const tile = @import("tiles").tile; const mvt = @import("tiles").mvt; const mlt = @import("tiles").mlt; const render = @import("render"); -const assets = @import("assets"); -const geometry = @import("geo"); // Martinez boolean; `geo` is a common param name here +const style = @import("style"); +const geometry = @import("geometry"); // Martinez boolean; `geo` is a common param name here const rs = render.surface; /// The banded multi-cell ENC_ROOT -> PMTiles baker (folded in: it is the /// batch driver of this engine). Re-exported for the CLI + lib root. pub const bake_enc = @import("bake_enc.zig"); - -/// SCAMIN standalone (specs/scamin-standalone.md): cross-cell point-object -/// matching + SCAMIN union + scale-window eligibility for the *_scamin point/ -/// text layers. Shared by the bake pre-pass and the live per-tile dedup. -/// Output tile encoding: classic Mapbox Vector Tile, or MapLibre Tile (optional). +pub const coverage = @import("coverage"); // per-cell coverage sidecar (in PMTiles metadata) +const symins = @import("symins.zig"); // SYMINS02 native fallback (NEWOBJ producer draw ops) + +// Light-sector reach (scene/lightreach.zig), re-exported so chart + bake_enc keep +// reaching it through the scene module. +const lightreach = @import("lightreach.zig"); +pub const LightReach = lightreach.LightReach; +pub const collectLightReach = lightreach.collectLightReach; +pub const lightReachTiles = lightreach.lightReachTiles; +pub const LIGHT_AUG_REACH_TILES = lightreach.LIGHT_AUG_REACH_TILES; + +// Complex-linestyle tessellation + registry (scene/linestyle.zig). Exposed so +// chart + bundle register and look up styles through `scene.linestyle`. +pub const linestyle = @import("linestyle.zig"); + +// Baked-tile replay (scene/replay.zig): a decoded tile -> Surface draw calls. +const replay = @import("replay.zig"); +pub const replayTile = replay.replayTile; + +/// Output tile encoding: classic Mapbox Vector Tile, or MapLibre Tile. pub const TileFormat = enum { mvt, mlt }; -const s101 = @import("s100").s101_instr; -const catalogue = @import("s100").catalogue; -const s101_adapt = @import("s100").s101_adapt; +const instructions = @import("s101").instructions; +const catalogue = @import("s101").catalogue; +const adapter = @import("s101").adapter; // S-52 symbol scale the Go baker emits for every point symbol / sounding. The // style's icon-size = scale / ATLAS_PPU (0.08), so this renders symbols at @@ -40,141 +60,10 @@ const SYMBOL_SCALE: f64 = @import("render").sndfrm.SYMBOL_SCALE; // so the generated style can show soundings in feet when the mariner picks that unit — // a recreational-chartplotter convenience, not ECDIS behaviour. The feet value runs // through the same SNDFRM04 glyph composition as metres, so it keeps one decimal place -// for shallow soundings (the depth-contour feet label, chartstyle.contourLabelField, +// for shallow soundings (the depth-contour feet label, mariner.contourLabelField, // rounds to whole feet — contour valdco values are whole metres). const M_TO_FT: f64 = @import("render").sndfrm.M_TO_FT; -// Worst-case reach of a light's sector legs/arcs (emitAugFigures) as a fraction of a -// tile — these are drawn at a fixed DISPLAY size (radius/length in mm), so the reach -// is ~constant in tile units at every zoom (offset_tiles = mm * PX_PER_MM / 256). -// Used to widen the LIGHTS spatial-cull margin so an arc isn't dropped on the tiles it -// crosses (S-52 legs ~25 mm / arcs ~20 mm ≈ 0.8 tile; 1.0 leaves headroom). -// GROUND-length legs (directional lights: nmi2metres(nominal range), LightSectored.lua) -// exceed this by far at fine zooms — lightReachTiles is the honest per-zoom bound. -pub const LIGHT_AUG_REACH_TILES: f64 = 1.0; - -/// How far a cell's constructed sector figures (emitAugFigures) can reach beyond -/// their feature anchors, summarised per cell so tile addressing (bake_enc -/// buildTileMap / the live tileRefs) can include the cell in neighbouring tiles -/// its raw bbox never touches — otherwise legs/arcs clip exactly at the boundary. -pub const LightReach = struct { - /// Union bbox [w,s,e,n] of the aug-figure-bearing features' anchors (feature - /// nodes + explicit AugmentedPoint anchors); null = no sector figures at all. - bbox: ?[4]f64 = null, - /// Max ground-distance leg length in metres (AugmentedRay with a GeographicCRS - /// length — directional-light legs); 0 = display-mm figures only. - range_m: f64 = 0, -}; - -/// Worst-case tile-unit reach of sector figures at zoom z, latitude `lat`: -/// display-mm figures reach a ~constant LIGHT_AUG_REACH_TILES, ground-length legs -/// reach range_m metres = range_m·2^z/(cosφ·C) tiles (the emitAugFigures -/// projection: len_px = range_m/(cosφ·EARTH_CIRCUM_M)·worldPx). Never below the -/// mm bound — a cell with figures always reaches at least the display-sized ones. -pub fn lightReachTiles(range_m: f64, z: u8, lat: f64) f64 { - if (range_m <= 0) return LIGHT_AUG_REACH_TILES; - const cos_lat = @max(@cos(lat * std.math.pi / 180.0), 1e-6); - const scale: f64 = @floatFromInt(@as(u64, 1) << @intCast(z)); - return @max(LIGHT_AUG_REACH_TILES, range_m * scale / (cos_lat * EARTH_CIRCUM_M)); -} - -// The nth comma-separated field of an instruction argument ("" past the end). -fn instrCsv(s: []const u8, n: usize) []const u8 { - var it = std.mem.splitScalar(u8, s, ','); - var i: usize = 0; - while (it.next()) |part| : (i += 1) if (i == n) return std.mem.trim(u8, part, " "); - return ""; -} - -// Fold one aug-figure-bearing feature's anchors + ground length into `r`: -// the feature node plus any explicit AugmentedPoint anchors in the stream. -fn foldLightReach(r: *LightReach, cell: *const s57.Cell, f: s57.Feature, stream: []const u8) void { - var b: [4]f64 = if (r.bbox) |bb| bb else .{ 1e9, 1e9, -1e9, -1e9 }; - if (cell.pointGeometry(f)) |pg| { - b[0] = @min(b[0], pg.lon()); - b[1] = @min(b[1], pg.lat()); - b[2] = @max(b[2], pg.lon()); - b[3] = @max(b[3], pg.lat()); - } - var it = std.mem.splitScalar(u8, stream, ';'); - while (it.next()) |item| { - const colon = std.mem.indexOfScalar(u8, item, ':') orelse continue; - const key = item[0..colon]; - const val = item[colon + 1 ..]; - if (std.mem.eql(u8, key, "AugmentedRay")) { - // "AugmentedRay:,,," — a GeographicCRS - // length is ground metres (the directional-light leg); else display mm. - if (std.mem.eql(u8, instrCsv(val, 2), "GeographicCRS")) { - const len = std.fmt.parseFloat(f64, instrCsv(val, 3)) catch 0; - r.range_m = @max(r.range_m, len); - } - } else if (std.mem.eql(u8, key, "AugmentedPoint")) { - // "AugmentedPoint:,," — an explicit figure anchor. - const lon = std.fmt.parseFloat(f64, instrCsv(val, 1)) catch continue; - const lat = std.fmt.parseFloat(f64, instrCsv(val, 2)) catch continue; - b[0] = @min(b[0], lon); - b[1] = @min(b[1], lat); - b[2] = @max(b[2], lon); - b[3] = @max(b[3], lat); - } - } - if (b[0] <= b[2]) r.bbox = b; -} - -/// EXACT per-cell sector-figure reach, from the portrayal instruction streams: -/// a feature constructs figures iff its stream carries AugmentedRay/ArcByRadius -/// (LightSectored legs/arcs), so this can't drift from what emitAugFigures will -/// actually draw (including context-parameter effects like FullLightLines). -/// Only prim==1 features can emit figures (processFeatureParsed). Allocation-free. -pub fn collectLightReach(cell: *const s57.Cell, portrayal: ?[]const ?[]const u8) LightReach { - var r = LightReach{}; - const streams = portrayal orelse return r; - for (cell.features, 0..) |f, fi| { - if (f.prim != 1 or fi >= streams.len) continue; - const stream = streams[fi] orelse continue; - if (std.mem.indexOf(u8, stream, "AugmentedRay:") == null and - std.mem.indexOf(u8, stream, "ArcByRadius:") == null) continue; - foldLightReach(&r, cell, f, stream); - } - return r; -} - -/// CONSERVATIVE per-cell sector-figure reach from the raw S-57 attributes, for -/// paths that have a parsed cell but no portrayal yet (the bundle baker's -/// pre-pass super-tile index + planned-tile estimate). Must be a SUPERSET of -/// collectLightReach under the default portrayal context: any LIGHTS point is a -/// figure candidate (sector legs/arcs, all-round major-light rings), and a -/// directional light (CATLIT 1 + ORIENT) draws a nmi2metres(VALNMR || 9) ground -/// leg (LightSectored.lua). A non-default FullLightLines context could still -/// exceed this for plain sectored lights — the bake default keeps it off. -pub fn scanLightReachAttrs(cell: *const s57.Cell) LightReach { - var r = LightReach{}; - for (cell.features) |f| { - if (f.objl != 75 or f.prim != 1) continue; // LIGHTS points only - var b: [4]f64 = if (r.bbox) |bb| bb else .{ 1e9, 1e9, -1e9, -1e9 }; - const pg = cell.pointGeometry(f) orelse continue; - b[0] = @min(b[0], pg.lon()); - b[1] = @min(b[1], pg.lat()); - b[2] = @max(b[2], pg.lon()); - b[3] = @max(b[3], pg.lat()); - r.bbox = b; - // Directional (CATLIT list contains 1) with an orientation: the rule - // always draws the full nominal-range ground leg. - const catlit = f.attr(s57.ATTR_CATLIT) orelse ""; - var directional = false; - var it = std.mem.splitScalar(u8, catlit, ','); - while (it.next()) |v| { - if (std.mem.eql(u8, std.mem.trim(u8, v, " "), "1")) directional = true; - } - const orient: []const u8 = f.attr(s57.ATTR_ORIENT) orelse ""; - if (directional and orient.len > 0) { - const nmi = f.attrFloat(s57.ATTR_VALNMR) orelse 9.0; - r.range_m = @max(r.range_m, nmi * 1852.0); - } - } - return r; -} - // S-57 attribute code for SCAMIN (the minimum display scale 1:N, S-57 Appendix A // attr 133 / S-52 §8.4). Features carrying it are routed to a dedicated *_scamin // MVT layer so the style can drop them below their 1:N scale; the value travels on @@ -216,7 +105,7 @@ fn quaposSolidClass(objl: u16) bool { } /// True if the S-57 comma-separated list value `csv` contains any of `targets`. -/// (Mirrors s101_adapt.hasListVal; S-57 list attributes are comma-joined.) +/// (Mirrors adapter.hasListVal; S-57 list attributes are comma-joined.) fn listHasAny(csv: []const u8, targets: []const i64) bool { var it = std.mem.splitScalar(u8, csv, ','); while (it.next()) |p| { @@ -315,7 +204,7 @@ fn clipSimplifyPoly(a: Allocator, proj: []const mvt.Point, box: tile.Box) ![]con return if (ring.len >= 3) ring else clipped[0..0]; } -// Coverage-clipped best-available composite (specs/cross-band-composition-redesign.md): +// Coverage-clipped best-available composite: // subtract the finer-scale coverage `clip` (already projected + box-clipped into this // tile's i32 space, a clean union) from a coarser cell's box-clipped area rings, so a // feature is emitted by exactly one cell — the finest whose M_COVR covers it. Replaces @@ -440,112 +329,6 @@ fn coveredByFiner(cover_clip: []const []const mvt.Point, lon: f64, lat: f64, z: } // Clip a line + simplify each kept run (drop runs that collapse below 2 vertices). -fn clipSimplifyLine(a: Allocator, proj: []const mvt.Point, box: tile.Box) ![]const []const mvt.Point { - const sub = try tile.clipLine(a, proj, box); - var out = std.ArrayList([]const mvt.Point).empty; - for (sub) |run| { - const s = try tile.simplifyRing(a, run); - if (s.len >= 2) try out.append(a, s); - } - return out.items; -} - -/// Shoelace signed area (x2) of a ring in tile space; only its sign is used. -/// y is down, so a positive value is a clockwise (exterior) ring per the MVT spec. -fn ringSignedArea(ring: []const mvt.Point) i64 { - if (ring.len < 3) return 0; - var area: i64 = 0; - var j: usize = ring.len - 1; - for (ring, 0..) |p, i| { - const q = ring[j]; - area += @as(i64, q.x) * @as(i64, p.y) - @as(i64, p.x) * @as(i64, q.y); - j = i; - } - return area; -} - -/// Even-odd ray test: is tile-space point `pt` inside `ring`? -fn ringContains(ring: []const mvt.Point, pt: mvt.Point) bool { - if (ring.len < 3) return false; - var inside = false; - const px: f64 = @floatFromInt(pt.x); - const py: f64 = @floatFromInt(pt.y); - var j: usize = ring.len - 1; - for (ring, 0..) |p, i| { - const q = ring[j]; - const ax: f64 = @floatFromInt(p.x); - const ay: f64 = @floatFromInt(p.y); - const bx: f64 = @floatFromInt(q.x); - const by: f64 = @floatFromInt(q.y); - if ((ay > py) != (by > py) and - px < (bx - ax) * (py - ay) / (by - ay) + ax) - { - inside = !inside; - } - j = i; - } - return inside; -} - -/// Orient + order a feature's clipped area rings into MVT multipolygon parts so -/// holes are SUBTRACTED instead of filled (e.g. an island inside a sea/depth -/// area). Mirrors the Go reference encodePolygon: classify each ring by geometric -/// nesting depth (even = exterior, odd = hole), force exteriors to a positive -/// signed area (clockwise in y-down tile space) and holes to negative, and emit -/// each exterior immediately followed by the holes it directly contains. This is -/// independent of the FSPT USAG tags, and keeps disjoint multi-part areas -/// (multiple exteriors) working as a proper multipolygon. `rings` are the clipped -/// rings (open, >= 3 pts); returned parts may reverse a ring into a fresh copy. -fn orientAreaRings(a: Allocator, rings: []const []const mvt.Point) ![]const []const mvt.Point { - const n = rings.len; - const depth = try a.alloc(usize, n); - for (rings, 0..) |ri, i| { - var d: usize = 0; - for (rings, 0..) |rj, j| { - if (i != j and ringContains(rj, ri[0])) d += 1; - } - depth[i] = d; - } - - const done = try a.alloc(bool, n); - @memset(done, false); - var out = std.ArrayList([]const mvt.Point).empty; - - const emit = struct { - fn one(al: Allocator, list: *std.ArrayList([]const mvt.Point), ring: []const mvt.Point, d: usize) !void { - const want_pos = (d % 2) == 0; // even depth = exterior (positive), odd = hole - if ((ringSignedArea(ring) >= 0) == want_pos) { - try list.append(al, ring); - } else { - const rev = try al.alloc(mvt.Point, ring.len); - for (ring, 0..) |p, k| rev[ring.len - 1 - k] = p; - try list.append(al, rev); - } - } - }.one; - - // Each exterior (even depth) followed by the holes it directly contains, so a - // decoder attaches each hole to the right exterior (depth exactly +1, inside). - for (0..n) |i| { - if (done[i] or depth[i] % 2 != 0) continue; - done[i] = true; - try emit(a, &out, rings[i], depth[i]); - for (0..n) |k| { - if (done[k] or depth[k] != depth[i] + 1) continue; - if (ringContains(rings[i], rings[k][0])) { - done[k] = true; - try emit(a, &out, rings[k], depth[k]); - } - } - } - // Safety net: emit anything not placed (malformed nesting) on its own. - for (0..n) |i| { - if (done[i]) continue; - done[i] = true; - try emit(a, &out, rings[i], depth[i]); - } - return out.items; -} fn geomBounds(g: []const s57.LonLat) [4]f64 { var b = [4]f64{ 1e9, 1e9, -1e9, -1e9 }; @@ -599,7 +382,7 @@ pub const CellOpts = struct { /// and emitted as the AP(OVERSC01) overscale hatch's gate (hatch shows while /// the display denominator is FINER/smaller than oscl). See emitOverscaleHatch. oscl: i64 = 0, - /// Scale-boundary overscale (specs/overscale.md, S-52 §10.1.10.2): emit this + /// Scale-boundary overscale (S-52 §10.1.10.2): emit this /// cell's AP(OVERSC01) coverage hatch into the tile. Set only when BOTH (a) a /// strictly-finer-CSCL cell also contributes to the SAME tile — a scale /// boundary exists (else whole-view overscale, the HUD "overscale ×n" readout's @@ -650,7 +433,7 @@ pub const TileSurface = struct { texts: std.ArrayList(mvt.Feature) = .empty, // SCAMIN buckets: a feature carrying SCAMIN (s57 attr 133) routes here instead // of the base list, and carries a `scamin` property so the style gates its - // display below the feature's 1:N scale (see ATTR_SCAMIN / assets/style.zig). + // display below the feature's 1:N scale (see ATTR_SCAMIN / style/maplibre.zig). areas_scamin: std.ArrayList(mvt.Feature) = .empty, area_patterns_scamin: std.ArrayList(mvt.Feature) = .empty, lines_scamin: std.ArrayList(mvt.Feature) = .empty, @@ -676,7 +459,6 @@ pub const TileSurface = struct { .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 { @@ -722,6 +504,7 @@ pub const TileSurface = struct { .s57 = meta.s57_json, .cell = meta.cell_name, .band = meta.band, + .masked = meta.masked, .date_start = meta.date_start, .date_end = meta.date_end, .bnd = meta.bnd, @@ -740,7 +523,7 @@ pub const TileSurface = struct { // The cell's quantized compilation scale: the style's overscale machinery // splits area fills into a below-the-hatch (overscaled) and an above-the- // hatch (at-scale) pass on this tag — so a finer cell's opaque fill - // occludes a coarser cell's OVERSC01 hatch (specs/overscale.md). + // occludes a coarser cell's OVERSC01 hatch. if (s.cur.oscl > 0) try props.append(s.a, .{ .key = "oscl", .value = .{ .int = s.cur.oscl } }); try appendMeta(s.a, &props, s.cur); try s.areasL().append(s.a, .{ .geom_type = .polygon, .parts = rings, .properties = props.items }); @@ -772,22 +555,6 @@ 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; @@ -833,10 +600,10 @@ pub const TileSurface = struct { try s.soundings.append(s.a, .{ .geom_type = .point, .parts = parts, .properties = props.items }); } - fn drawText(ctx: *anyopaque, text: []const u8, style: *const rs.TextStyle, at: rs.TilePoint) anyerror!void { + fn drawText(ctx: *anyopaque, text: []const u8, text_style: *const rs.TextStyle, at: rs.TilePoint) anyerror!void { const s = sp(ctx); var props = std.ArrayList(mvt.Prop).empty; - try appendTextProps(s.a, &props, text, style); // text already shortened by engine + try appendTextProps(s.a, &props, text, text_style); // text already shortened by engine try appendMeta(s.a, &props, s.cur); const parts = try s.a.alloc([]const mvt.Point, 1); const single = try s.a.alloc(mvt.Point, 1); @@ -934,24 +701,23 @@ test "hasAdditionalInfo: INFORM/TXTDSC trigger; blank/absent/other don't" { try std.testing.expect(!hasAdditionalInfo(.{ .rcnm = 100, .rcid = 5, .prim = 1, .objl = 75, .attrs = &.{.{ .code = inform, .value = " \t " }} })); } -/// The vector layers this engine emits, in emit order — the source-layer ids the -/// generated MapLibre style reads. v2 tile schema (tile57/2): 6 layers, one per -/// render family. Each former `_scamin` twin is folded into its base at emit time -/// (endScene) — SCAMIN is now a per-feature `scamin` property the style gates, not a -/// separate layer — and complex/sector lines fold into `lines`. Static: an archive -/// may omit empties, but the TileJSON advertises the full set. Keep in sync with the -/// layer appends in endScene. -pub const VECTOR_LAYERS = [_][]const u8{ - "areas", "area_patterns", "lines", "point_symbols", "soundings", "text", -}; +/// The vector layers this engine emits, in emit order — the tile57/2 source-layer +/// set. Defined in tiles.mvt (shared with the compositor) and re-exported here; +/// keep the layer appends in endScene in sync with it. +pub const VECTOR_LAYERS = mvt.VECTOR_LAYERS; /// PMTiles archive metadata JSON: the static vector_layers list MapLibre reads from /// the TileJSON, plus a "scamin" array of the distinct SCAMIN denominators present /// (ascending) so the client builds one native-minzoom bucket layer per value at -/// load (host-canonical-backend.md §2) instead of probing tiles. Mirrors the Go -/// pmtiles.Builder.metadata (vector_layers + scamin splice). `scamin` empty -> omit -/// the field. Caller owns the returned bytes (allocated in `a`). -pub fn metadataJson(a: Allocator, scamin: []const u32) ![]const u8 { +/// load instead of probing tiles. Mirrors the Go pmtiles.Builder.metadata +/// (vector_layers + scamin splice). `scamin` empty -> omit the field. `coverage_json`, +/// when non-null, is a per-cell coverage object (see `coverage.encodeJson`) spliced +/// under a "coverage" key — a single-cell composite bake carries its own M_COVR there. +/// `light_reach_json` (see `coverage.encodeLightReachJson`), when non-null, is the +/// cell's sector-figure reach summary spliced under a "light_reach" key so the +/// compositor can widen its tile addressing without re-portraying the cell. +/// Caller owns the returned bytes (allocated in `a`). +pub fn metadataJson(a: Allocator, scamin: []const u32, coverage_json: ?[]const u8, light_reach_json: ?[]const u8) ![]const u8 { var b = std.ArrayList(u8).empty; try b.appendSlice(a, "{\"name\":\"chartplotter\",\"format\":\"pbf\",\"vector_layers\":["); for (VECTOR_LAYERS, 0..) |name, i| { @@ -970,6 +736,14 @@ pub fn metadataJson(a: Allocator, scamin: []const u32) ![]const u8 { } try b.append(a, ']'); } + if (coverage_json) |cj| { + try b.appendSlice(a, ",\"coverage\":"); + try b.appendSlice(a, cj); + } + if (light_reach_json) |lj| { + try b.appendSlice(a, ",\"light_reach\":"); + try b.appendSlice(a, lj); + } try b.append(a, '}'); return b.toOwnedSlice(a); } @@ -995,6 +769,7 @@ const Meta = struct { // (unknown cell name, or pick attributes disabled). From s57.Cell.name. cell: []const u8 = "", band: u8 = 0, // NOAA band rank (0 finest … 5 coarsest) + masked: bool = false, // S-52 §8.6.2 suppressed boundary piece (meta-bounds view only) date_start: []const u8 = "", date_end: []const u8 = "", // S-52 boundary symbolization (§8.6.1) and point-symbol style (§11.2.2) tags @@ -1014,31 +789,6 @@ const Meta = struct { // modifier, or 12 by default), halign/valign (the resolved TextAlign values the // style's TEXT_ANCHOR keys on), and the §14.5 text group. Halo + offset are // separate findings (still on the OpText halo / LocalOffset rows). -/// Case-insensitive last occurrence of `needle` in `haystack`. -fn lastIndexOfCI(haystack: []const u8, needle: []const u8) ?usize { - if (needle.len == 0 or needle.len > haystack.len) return null; - var i: usize = haystack.len - needle.len + 1; - while (i > 0) { - i -= 1; - if (std.ascii.eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return i; - } - return null; -} - -/// Reduce a buoy/lighted-buoy name label to its chart designation. The S-101 buoy -/// rules tag the feature "by " (EncodeString 'by %s'); NOAA OBJNAM is the full -/// descriptive name ("Chesapeake Channel Lighted Buoy 78A") but a chart shows only the -/// trailing designation ("78A"), which also de-clutters a channel of buoys whose long -/// prefix repeats. The "by " prefix marks these name labels; all other text (depth -/// labels, light elevations, …) has no such prefix and passes through unchanged. The -/// designation is whatever follows the LAST buoy/beacon type-word; "Light"/"Lt" are -/// deliberately excluded so a named light keeps its name, and a name with no type-word -/// keeps its stripped form (e.g. a bare "22"). -/// -/// An extracted designation is QUOTED ("78A") like the paper chart: an aid's -/// designation in quotes cannot be misread as a sounding or a depth — the -/// chart convention exists for exactly that reason. Unshortened text passes -/// through borrowed; a quoted designation allocates from `a`. // SBDARE nature-of-surface labels arrive from the (pristine, vendored) S-101 // rule as INT1 abbreviations ("S", "M", "Cy" …, space-joined for multiple // surfaces). A screen has no chart-margin legend to decode them against, so @@ -1076,40 +826,30 @@ fn expandSeabedText(a: Allocator, class: []const u8, text: []const u8) ![]const return out.toOwnedSlice(a); } -fn shortenName(a: Allocator, text: []const u8) ![]const u8 { - // "by " = buoy names, "bn " = beacon names (EncodeString prefixes in the - // buoy/beacon rules); both reduce to the designation. - const tagged = std.mem.startsWith(u8, text, "by ") or std.mem.startsWith(u8, text, "bn "); - if (!tagged) return text; - const name = std.mem.trim(u8, text[3..], " "); - const keywords = [_][]const u8{ "Daybeacon", "Daymark", "Buoy", "Beacon" }; - var best_end: ?usize = null; - for (keywords) |kw| { - if (lastIndexOfCI(name, kw)) |idx| { - const end = idx + kw.len; - if (best_end == null or end > best_end.?) best_end = end; - } - } - if (best_end) |end| { - const rest = std.mem.trim(u8, name[end..], " "); - if (rest.len > 0) return std.fmt.allocPrint(a, "\"{s}\"", .{rest}); - } - return name; +/// Strip the "by "/"bn " aid-name tag the S-101 buoy/beacon rules add (EncodeString +/// 'by %s' / 'bn %s') and show the FULL name. Previously this reduced the name to its +/// trailing chart designation ("Chesapeake Channel Lighted Buoy 78A" -> "78A"); that +/// shortening was removed so aid names read in full. Untagged text (depth labels, light +/// elevations, place names) passes through unchanged. Returns a borrowed slice. +fn stripNameTag(text: []const u8) []const u8 { + if (std.mem.startsWith(u8, text, "by ") or std.mem.startsWith(u8, text, "bn ")) + return std.mem.trim(u8, text[3..], " "); + return text; } /// Serialize a text label's props in the tile schema order. `text` arrives already /// shortened/resolved by the engine. A minimal label (empty halign — see /// rs.TextStyle) carries only text/color/size, as the native fallbacks always did. -fn appendTextProps(a: Allocator, props: *std.ArrayList(mvt.Prop), text: []const u8, style: *const rs.TextStyle) !void { +fn appendTextProps(a: Allocator, props: *std.ArrayList(mvt.Prop), text: []const u8, text_style: *const rs.TextStyle) !void { // Resolved body size: the FontSize modifier px, or 12 (oracle default). Drives // both the emitted font_size_px and the halo gate below. - const font_px: f64 = if (style.font_size > 0) style.font_size else 12; + const font_px: f64 = if (text_style.font_size > 0) text_style.font_size else 12; try props.append(a, .{ .key = "text", .value = .{ .string = text } }); - try props.append(a, .{ .key = "color_token", .value = .{ .string = style.color } }); + try props.append(a, .{ .key = "color_token", .value = .{ .string = text_style.color } }); try props.append(a, .{ .key = "font_size_px", .value = .{ .double = font_px } }); - if (style.halign.len == 0) return; // minimal label: no alignment/halo/group spec - try props.append(a, .{ .key = "halign", .value = .{ .string = style.halign } }); - try props.append(a, .{ .key = "valign", .value = .{ .string = style.valign } }); + if (text_style.halign.len == 0) return; // minimal label: no alignment/halo/group spec + try props.append(a, .{ .key = "halign", .value = .{ .string = text_style.halign } }); + try props.append(a, .{ .key = "valign", .value = .{ .string = text_style.valign } }); // S-101 LocalOffset -> label-offset key in text-body units (3.51 mm = one text // body height = 1 em): the style's text-offset match keys on "ux,uy" to shift a // name clear of its symbol (PortrayFeatureName emits 0,-3.51 = one body up). @@ -1117,21 +857,21 @@ fn appendTextProps(a: Allocator, props: *std.ArrayList(mvt.Prop), text: []const // right / +y down, which matches MapLibre's text-offset. { const TEXT_BODY_MM = 3.51; - const ux: i64 = @intFromFloat(@round(style.offset_x / TEXT_BODY_MM)); - const uy: i64 = @intFromFloat(@round(style.offset_y / TEXT_BODY_MM)); + const ux: i64 = @intFromFloat(@round(text_style.offset_x / TEXT_BODY_MM)); + const uy: i64 = @intFromFloat(@round(text_style.offset_y / TEXT_BODY_MM)); if (ux != 0 or uy != 0) try props.append(a, .{ .key = "loff", .value = .{ .string = try std.fmt.allocPrint(a, "{d},{d}", .{ ux, uy }) } }); } // §10 text halo: the oracle attaches a CHWHT, 1px halo to text >= 10 px // (s101emit.go:130-133) and emits it as halo_color_token / halo_width tile // properties (bake.go:1147-1148); smaller text gets no halo ("" / 0). Our served - // style still renders text solid (no halo) by deliberate S-52 choice (style.zig + // style still renders text solid (no halo) by deliberate S-52 choice (maplibre.zig // textPaint passes halo_width 0) — these are tile-parity properties for a client // that wants the legibility halo. const haloed = font_px >= 10; try props.append(a, .{ .key = "halo_color_token", .value = .{ .string = if (haloed) "CHWHT" else "" } }); try props.append(a, .{ .key = "halo_width", .value = .{ .double = if (haloed) 1 else 0 } }); - try props.append(a, .{ .key = "tgrp", .value = .{ .int = style.group } }); + try props.append(a, .{ .key = "tgrp", .value = .{ .int = text_style.group } }); } /// JSON-escape `s` (surrounding quotes included) into `buf`: escapes ", \, and the @@ -1226,6 +966,9 @@ fn appendMeta(a: Allocator, props: *std.ArrayList(mvt.Prop), m: Meta) !void { // unvarying feature's tile footprint unchanged. if (m.bnd != 2) try props.append(a, .{ .key = "bnd", .value = .{ .int = m.bnd } }); if (m.pts != 2) try props.append(a, .{ .key = "pts", .value = .{ .int = m.pts } }); + // §8.6.2-suppressed boundary piece — present only on the meta-bounds extras, + // so every normally-drawn feature's tile footprint is unchanged. + if (m.masked) try props.append(a, .{ .key = "masked", .value = .{ .int = 1 } }); // Date-dependent display (S-52 §10.4.1.1): recurring iff a "--" month-day prefix, // stripped so the client compares MMDD (recurring) / YYYYMMDD (fixed). if (m.date_start.len > 0 or m.date_end.len > 0) { @@ -1330,10 +1073,15 @@ pub fn buildLabelCache(a: Allocator, cell: *const s57.Cell, geo: ?GeoParts, stre var tmp = std.heap.ArenaAllocator.init(a); defer tmp.deinit(); for (cell.features, 0..) |f, i| { - if (f.prim != 2 and f.prim != 3) continue; + // Polylabel (areaRepresentativePoint) is AREA-only — lines anchor at their mid-vertex. + // Cache every portrayed area: labelPoint is consulted not just for Text/PointInstruction + // labels but also for the native INFORM01/QUESMRK1 centred markers (which carry no such + // instruction), so the earlier Text/Point-only gate left those recomputing per tile — + // the dominant cost on large coarse cells. Byte-identical: labelPoint returns the same + // point cached or live. + if (f.prim != 3) continue; if (i >= ss.len) break; - const s = ss[i] orelse continue; - if (std.mem.indexOf(u8, s, "TextInstruction") == null and std.mem.indexOf(u8, s, "PointInstruction") == null) continue; + _ = ss[i] orelse continue; // portrayed at all (an unportrayed area never reaches labelPoint) const parts = featureParts(tmp.allocator(), cell.*, geo, i, f) catch continue; out[i] = s57.areaRepresentativePoint(tmp.allocator(), parts); _ = tmp.reset(.retain_capacity); @@ -1489,11 +1237,11 @@ fn variantDiffers(base: []const u8, variant: ?[]const u8) bool { /// processFeatureParsed, splitting into two passes only when a variant differs /// (S-52 boundary §8.6.1 / point-symbol §11.2.2 axes -> the bnd/pts tags). fn processFeatureInstr(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, geo_world: ?GeoWorld, instr: []const u8, plain: ?[]const u8, simplified: ?[]const u8, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { - const base = try s101.parse(a, instr); + const base = try instructions.parse(a, instr); if (f.prim == 1) { if (variantDiffers(instr, simplified)) { try processFeatureParsed(a, cell, f, fi, geo, geo_world, base, 2, 0, z, x, y, tb, box, opts, surf); - const sp2 = try s101.parse(a, simplified.?); + const sp2 = try instructions.parse(a, simplified.?); try processFeatureParsed(a, cell, f, fi, geo, geo_world, sp2, 2, 1, z, x, y, tb, box, opts, surf); } else { try processFeatureParsed(a, cell, f, fi, geo, geo_world, base, 2, 2, z, x, y, tb, box, opts, surf); @@ -1502,7 +1250,7 @@ fn processFeatureInstr(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, } if (f.prim == 3 and variantDiffers(instr, plain)) { try processFeatureParsed(a, cell, f, fi, geo, geo_world, base, 1, 2, z, x, y, tb, box, opts, surf); - const pl = try s101.parse(a, plain.?); + const pl = try instructions.parse(a, plain.?); try processFeatureParsed(a, cell, f, fi, geo, geo_world, pl, 0, 2, z, x, y, tb, box, opts, surf); return; } @@ -1535,10 +1283,16 @@ fn bearingToScreen(deg: f64) [2]f64 { /// (bake_enc.bandZooms(.approach).min = 11). Partial sector arcs are unaffected. const RING_MIN_ZOOM: u8 = 11; -fn emitAugFigures(a: Allocator, figs: []const s101.AugFigure, anchor: s57.LonLat, fmeta: rs.FeatureMeta, z: u8, x: u32, y: u32, box: tile.Box, surf: rs.Surface) !void { +fn emitAugFigures(a: Allocator, figs: []const instructions.AugFigure, anchor: s57.LonLat, fmeta: rs.FeatureMeta, z: u8, x: u32, y: u32, box: tile.Box, surf: rs.Surface) !void { if (figs.len == 0) return; - const world_px = 256.0 * @as(f64, @floatFromInt(@as(u64, 1) << @intCast(z))); - const pxmm = s101.PX_PER_MM; + // One tile renders as 512 CSS px (the engine's physical-scale model — style + // scaminGateK's M_PER_PX_Z0 is the 512-tile metres-per-CSS-px, and MapLibre + // lays vector tiles out at 512 logical px), so a display-mm length converts + // to a tile fraction against a 512·2^z world. The old 256-unit MVT world drew + // every figure at exactly 2x its catalogue size (20 mm sector arcs read + // 40 mm on the map). Ground-metre legs are a world-fraction ratio either way. + const world_px = 512.0 * @as(f64, @floatFromInt(@as(u64, 1) << @intCast(z))); + const pxmm = instructions.PX_PER_MM; for (figs) |fig| { const alon = if (fig.has_anchor) fig.anchor_lon else anchor.lon(); const alat = if (fig.has_anchor) fig.anchor_lat else anchor.lat(); @@ -1614,7 +1368,7 @@ fn appendDepthVals(a: Allocator, props: *std.ArrayList(mvt.Prop), f: s57.Feature /// every primitive with the pass's meta (draw_prio/cat/vg/scamin/bnd/pts + pick /// attrs). The engine work happens here — geometry assembly, projection, tile /// clipping/simplification, anchoring — so surfaces only ever see draw calls. -fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, geo_world: ?GeoWorld, p: s101.Portrayal, bnd: i64, pts: i64, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { +fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, geo_world: ?GeoWorld, p: instructions.Portrayal, bnd: i64, pts: i64, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { const scamin = effScamin(f, opts); const s57_json = if (opts.pick_attrs) try encodeS57Attrs(a, f) else ""; const cell_name = if (opts.pick_attrs) cell.name else ""; @@ -1677,7 +1431,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, try surf.endFeature(); } for (p.texts) |t| { - const style = rs.TextStyle{ + const ts = rs.TextStyle{ .color = t.color, .font_size = t.font_size, .halign = t.halign, @@ -1687,7 +1441,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, .group = t.group, }; try surf.beginFeature(&fmeta); - try surf.drawText(try expandSeabedText(a, fmeta.class, try shortenName(a, t.text)), &style, pt); + try surf.drawText(try expandSeabedText(a, fmeta.class, stripNameTag(t.text)), &ts, pt); try surf.endFeature(); } } @@ -1730,7 +1484,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, // Best-available: cut this cell's fill where a finer cell covers the ground. const arings = if (opts.cover_clip) |cc| subtractCoverage(a, rings.items, cc) else rings.items; if (arings.len > 0) { - const rparts = try orientAreaRings(a, arings); + const rparts = try mvt.orientAreaRings(a, arings); const dv = depthVals(f); const dr: ?rs.DepthRange = if (dv) |d| .{ .d1 = d[0], .d2 = d[1] } else null; const fillmeta = fmeta; @@ -1776,7 +1530,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, var want_geo = false; var want_proj = false; if (!opts.suppress_lines) for (p.lines) |ln| { - if (!std.mem.eql(u8, ln.style, "solid") and g_linestyles.get(ln.style) != null) want_geo = true else want_proj = true; + if (!std.mem.eql(u8, ln.style, "solid") and linestyle.lookup(ln.style) != null) want_geo = true else want_proj = true; }; if (want_geo) stroke_geo = clipGeoPartsOutsideCover(a, stroke_geo, cc, z, x, y); if (want_proj) stroke_proj = clipRunsOutsideCover(a, stroke_proj, cc); @@ -1786,9 +1540,9 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, if (!opts.suppress_lines) for (p.lines) |ln| { if (!std.mem.eql(u8, ln.style, "solid")) { - if (g_linestyles.get(ln.style)) |info| { + if (linestyle.lookup(ln.style)) |info| { // Complex linestyle: tessellated dash runs + tangent-rotated symbols. - try emitComplexLine(a, stroke_geo, info, ln.style, ln.color, !opts.suppress_points, z, x, y, box, &fmeta, surf); + try linestyle.drawComplexLine(a, stroke_geo, info, ln.style, ln.color, !opts.suppress_points, z, x, y, box, &fmeta, surf); continue; } } @@ -1797,15 +1551,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, else "dashed"; const dash: rs.Dash = if (std.mem.eql(u8, dash_str, "solid")) .solid else .dashed; - for (stroke_proj) |proj| { - const sub = try clipSimplifyLine(a, proj, box); - if (sub.len == 0) continue; - const seg_parts = try a.alloc([]const mvt.Point, sub.len); - for (sub, 0..) |seg, i| seg_parts[i] = seg; - try surf.beginFeature(&fmeta); - try surf.strokeLine(ln.color, ln.width, dash, seg_parts, valdco); - try surf.endFeature(); - } + try linestyle.drawPlainLine(a, stroke_proj, ln.color, ln.width, dash, box, &fmeta, valdco, surf); }; if (!opts.suppress_points and p.texts.len > 0) { @@ -1813,7 +1559,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, if (rp.lon() >= tb[0] and rp.lon() <= tb[2] and rp.lat() >= tb[1] and rp.lat() <= tb[3]) { const cpt = tile.project(rp.lon(), rp.lat(), z, x, y, tile.EXTENT); for (p.texts) |t| { - const style = rs.TextStyle{ + const ts = rs.TextStyle{ .color = t.color, .font_size = t.font_size, .halign = t.halign, @@ -1823,7 +1569,7 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, .group = t.group, }; try surf.beginFeature(&fmeta); - try surf.drawText(try expandSeabedText(a, fmeta.class, try shortenName(a, t.text)), &style, cpt); + try surf.drawText(try expandSeabedText(a, fmeta.class, stripNameTag(t.text)), &ts, cpt); try surf.endFeature(); } } @@ -1865,484 +1611,6 @@ fn processFeatureParsed(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, } } -// === SYMINS02 native fallback (S-52 PresLib §13.2.18 / §10.3.3.8) =========== -// Portray an S-57 NEWOBJ from its producer SYMINS attribute (code 192): a -// ';'-separated list of S-52 draw ops — SY()/TX()/TE()/LS()/LC()/AC()/AP() — -// rendered verbatim instead of the S-101 V-AIS alias the FeatureCatalogue maps -// NEWOBJ to. This is how the S-52 PresLib "ECDIS Chart 1" labels/boundaries/fills -// are drawn. Mirrors Go parseSYMINS (internal/engine/portrayal/symins.go). - -const SYMINS_ATTR: u16 = 192; - -fn syminsTrimQuotes(s: []const u8) []const u8 { - return std.mem.trim(u8, s, "'\""); -} - -fn syminsArgAt(args: []const []const u8, i: usize) []const u8 { - return if (i < args.len) args[i] else ""; -} - -/// The S-57 attribute value referenced by acronym (e.g. "OBJNAM"), trimmed, or null -/// when absent/blank. Mirrors Go lookupAttributeText over the feature's attrs. -fn syminsFeatAttr(f: s57.Feature, acr: []const u8) ?[]const u8 { - for (f.attrs) |at| { - const a2 = catalogue.attrAcronym(at.code) orelse continue; - if (std.ascii.eqlIgnoreCase(a2, acr)) { - const v = std.mem.trim(u8, at.value, " "); - return if (v.len == 0) null else v; - } - } - return null; -} - -/// Split a SYMINS string on ';', honouring quotes and nested parens (so a ';' inside -/// TX('a;b',…) or between parens isn't a split). Returns slices into `s`. -fn syminsSplitInstructions(a: Allocator, s: []const u8) ![]const []const u8 { - var out = std.ArrayList([]const u8).empty; - var depth: i32 = 0; - var in_quote = false; - var start: usize = 0; - var i: usize = 0; - while (i < s.len) : (i += 1) { - switch (s[i]) { - '\'', '"' => in_quote = !in_quote, - '(' => if (!in_quote) { - depth += 1; - }, - ')' => if (!in_quote) { - depth -= 1; - }, - ';' => if (!in_quote and depth == 0) { - try out.append(a, s[start..i]); - start = i + 1; - }, - else => {}, - } - } - if (start < s.len) try out.append(a, s[start..]); - return out.items; -} - -/// Split "OP(params)" into the op and inner params, or null when malformed. -fn syminsSplitOp(instr0: []const u8) ?struct { op: []const u8, params: []const u8 } { - const instr = std.mem.trim(u8, instr0, " \t"); - const open = std.mem.indexOfScalar(u8, instr, '(') orelse return null; - const close = std.mem.lastIndexOfScalar(u8, instr, ')') orelse return null; - if (open == 0 or close < open) return null; - return .{ .op = std.mem.trim(u8, instr[0..open], " \t"), .params = instr[open + 1 .. close] }; -} - -/// Split an instruction's params on ',', honouring quotes. Returns trimmed slices. -fn syminsSplitArgs(a: Allocator, params: []const u8) ![]const []const u8 { - var out = std.ArrayList([]const u8).empty; - var in_quote = false; - var start: usize = 0; - var i: usize = 0; - while (i < params.len) : (i += 1) { - const c = params[i]; - if (c == '\'' or c == '"') { - in_quote = !in_quote; - } else if (c == ',' and !in_quote) { - try out.append(a, std.mem.trim(u8, params[start..i], " \t")); - start = i + 1; - } - } - try out.append(a, std.mem.trim(u8, params[start..], " \t")); - return out.items; -} - -/// printf-style format substitution for a SYMINS TE() instruction (S-52 §3.2.3.2): -/// each %-spec consumes the next attribute name and formats its value (floats honour -/// .precision; integer convs round; the '0' flag + width zero-pad). Mirrors Go -/// formatSubstitute + appendConverted + zeroPad. Returns null when an attribute is -/// missing (the whole label is then dropped, as in Go). -fn syminsZeroPad(a: Allocator, out: *std.ArrayList(u8), s: []const u8, width: usize, flags: []const u8) !void { - const has0 = std.mem.indexOfScalar(u8, flags, '0') != null; - const has_minus = std.mem.indexOfScalar(u8, flags, '-') != null; - if (width <= s.len or !has0 or has_minus) { - try out.appendSlice(a, s); - return; - } - const pad = width - s.len; - const signed = s.len > 0 and (s[0] == '-' or s[0] == '+' or s[0] == ' '); - if (signed) try out.append(a, s[0]); - var k: usize = 0; - while (k < pad) : (k += 1) try out.append(a, '0'); - try out.appendSlice(a, if (signed) s[1..] else s); -} - -fn syminsAppendConverted(a: Allocator, out: *std.ArrayList(u8), val: []const u8, conv: u8, precision: i32, width: usize, flags: []const u8) !void { - var buf: [512]u8 = undefined; - var s: []const u8 = val; - switch (conv) { - 'f', 'e', 'g' => { - if (std.fmt.parseFloat(f64, std.mem.trim(u8, val, " \t"))) |x| { - s = std.fmt.float.render(&buf, x, .{ - .mode = .decimal, - .precision = if (precision >= 0) @as(usize, @intCast(precision)) else null, - }) catch val; - } else |_| {} - }, - 'd', 'i', 'u', 'x' => { - if (std.fmt.parseFloat(f64, std.mem.trim(u8, val, " \t"))) |x| { - const r: i64 = @intFromFloat(@round(x)); - s = std.fmt.bufPrint(&buf, "{d}", .{r}) catch val; - } else |_| {} - }, - else => {}, - } - try syminsZeroPad(a, out, s, width, flags); -} - -fn syminsFormatSubstitute(a: Allocator, f: s57.Feature, format: []const u8, names: []const []const u8) !?[]const u8 { - var out = std.ArrayList(u8).empty; - var attr_idx: usize = 0; - var i: usize = 0; - while (i < format.len) { - if (format[i] != '%' or i + 1 >= format.len) { - try out.append(a, format[i]); - i += 1; - continue; - } - if (format[i + 1] == '%') { - try out.append(a, '%'); - i += 2; - continue; - } - var j = i + 1; - const flags_start = j; - while (j < format.len and std.mem.indexOfScalar(u8, "-+ #0", format[j]) != null) j += 1; - const flags = format[flags_start..j]; - var width: usize = 0; - while (j < format.len and format[j] >= '0' and format[j] <= '9') : (j += 1) width = width * 10 + (format[j] - '0'); - var precision: i32 = -1; - if (j < format.len and format[j] == '.') { - j += 1; - var p: i32 = 0; - while (j < format.len and format[j] >= '0' and format[j] <= '9') : (j += 1) p = p * 10 + @as(i32, format[j] - '0'); - precision = p; - } - while (j < format.len and (format[j] == 'l' or format[j] == 'h' or format[j] == 'L')) j += 1; - if (j >= format.len) { - try out.appendSlice(a, format[i..]); // malformed trailing spec -> literal - break; - } - const conv = format[j]; - switch (conv) { - 's', 'c', 'd', 'i', 'u', 'x', 'f', 'e', 'g' => { - if (attr_idx >= names.len) return null; - const acr = names[attr_idx]; - attr_idx += 1; - const val = syminsFeatAttr(f, acr) orelse return null; - try syminsAppendConverted(a, &out, val, conv, precision, width, flags); - }, - else => try out.appendSlice(a, format[i .. j + 1]), // unknown conversion -> literal - } - i = j + 1; - } - return out.items; -} - -/// Parse a SYMINS TX()/TE() instruction into a Text. The Text model carries the -/// label string, colour and viewing group; the S-52 justification / offset / font / -/// halo fields are dropped (tracked OpText findings), matching the current text path. -fn syminsText(a: Allocator, f: s57.Feature, op: []const u8, params: []const u8) !?s101.Text { - const args = try syminsSplitArgs(a, params); - var text: []const u8 = ""; - var color_idx: usize = undefined; - var display_idx: usize = undefined; - if (std.mem.eql(u8, op, "TE")) { - if (args.len < 10) return null; - var names = std.ArrayList([]const u8).empty; - var it = std.mem.splitScalar(u8, syminsTrimQuotes(args[1]), ','); - while (it.next()) |nm| { - const t = std.mem.trim(u8, nm, " \t"); - if (t.len > 0) try names.append(a, t); - } - text = (try syminsFormatSubstitute(a, f, syminsTrimQuotes(args[0]), names.items)) orelse return null; - color_idx = 8; - display_idx = 9; - } else { // TX - if (args.len < 9) return null; - const raw = args[0]; - if (raw.len > 0 and (raw[0] == '\'' or raw[0] == '"')) { - text = syminsTrimQuotes(raw); // literal - } else { - text = syminsFeatAttr(f, std.mem.trim(u8, raw, " \t")) orelse return null; - } - color_idx = 7; - display_idx = 8; - } - if (text.len == 0) return null; - var color = std.mem.trim(u8, syminsArgAt(args, color_idx), " \t"); - if (color.len == 0) color = "CHBLK"; - const group = std.fmt.parseInt(i64, std.mem.trim(u8, syminsArgAt(args, display_idx), " \t"), 10) catch 0; - return s101.Text{ .text = text, .color = color, .group = group }; -} - -/// Build an S-101 Portrayal from a NEWOBJ's SYMINS attribute, or null when there is -/// no usable SYMINS (caller then falls back to the default new-object symbology). -/// Geometry/anchoring/clipping is handled by processFeatureParsed exactly like a rule stream. -fn buildSyminsPortrayal(a: Allocator, f: s57.Feature) !?s101.Portrayal { - const raw0 = f.attr(SYMINS_ATTR) orelse return null; - const raw = std.mem.trim(u8, raw0, " "); - if (raw.len == 0) return null; - - var points = std.ArrayList(s101.Point).empty; - var texts = std.ArrayList(s101.Text).empty; - var lines = std.ArrayList(s101.Line).empty; - var patterns = std.ArrayList([]const u8).empty; - var fill_token: ?[]const u8 = null; - - for (try syminsSplitInstructions(a, raw)) |instr| { - const opp = syminsSplitOp(instr) orelse continue; - if (std.mem.eql(u8, opp.op, "SY")) { // SY(NAME[,rot]) - const args = try syminsSplitArgs(a, opp.params); - const name = std.mem.trim(u8, syminsArgAt(args, 0), " \t"); - if (name.len == 0) continue; - const rot: f64 = if (args.len > 1) (std.fmt.parseFloat(f64, std.mem.trim(u8, args[1], " \t")) catch 0) else 0; - try points.append(a, .{ .symbol = name, .rotation = rot, .offset_x = 0, .offset_y = 0 }); - } else if (std.mem.eql(u8, opp.op, "TX") or std.mem.eql(u8, opp.op, "TE")) { - if (try syminsText(a, f, opp.op, opp.params)) |t| try texts.append(a, t); - } else if (std.mem.eql(u8, opp.op, "LS")) { // LS(style,width,colour) - const args = try syminsSplitArgs(a, opp.params); - if (args.len < 3) continue; - var w = std.fmt.parseInt(i64, std.mem.trim(u8, args[1], " \t"), 10) catch 0; - if (w <= 0) w = 1; - const st = std.mem.trim(u8, args[0], " \t"); - const dashed = std.ascii.eqlIgnoreCase(st, "DASH") or std.ascii.eqlIgnoreCase(st, "DOTT"); - try lines.append(a, .{ - .style = if (dashed) "dash" else "solid", - .width = @floatFromInt(w), - .color = std.mem.trim(u8, args[2], " \t"), - }); - } else if (std.mem.eql(u8, opp.op, "LC")) { // LC(LINESTYLE) — approximated as dashed - const name = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); - if (name.len == 0) continue; - try lines.append(a, .{ .style = name, .width = 1, .color = "CHBLK" }); - } else if (std.mem.eql(u8, opp.op, "AC")) { // AC(COLOUR[,transp]) - const color = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); - if (color.len > 0) fill_token = color; - } else if (std.mem.eql(u8, opp.op, "AP")) { // AP(PATTERN) - const name = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); - if (name.len > 0) try patterns.append(a, name); - } - } - if (points.items.len == 0 and texts.items.len == 0 and lines.items.len == 0 and - patterns.items.len == 0 and fill_token == null) return null; - return s101.Portrayal{ - .fill_token = fill_token, - .patterns = patterns.items, - .lines = lines.items, - .points = points.items, - .texts = texts.items, - }; -} - -// === Complex (symbolised) line tessellation (S-101 LineStyles) ============= -// A named linestyle (LC / a LineInstruction whose style is not "_simple_") is -// tessellated per zoom: walk the line by arc length and emit, per period, the dash -// "on" runs as line segments + each embedded symbol as a point rotated to the local -// tangent. Mirrors Go bake/complexline.go + linestyle_catalog.go. The mm geometry is -// parsed by assets.parseLineStyle; the baker converts it to LsInfo at the PresLib -// FEATURE scale (ls_px_per_mm) and registers it before baking. - -const ls_feature_scale: f64 = 0.01 / 0.35278; // px per 0.01-mm PresLib unit (= SYMBOL_SCALE) -const ls_px_per_mm: f64 = 100.0 * ls_feature_scale; // mm -> screen px -/// The mm->px feature scale the baker must apply when building an LsInfo from the raw -/// millimetre LineStyles geometry (assets.parseLineStyle), so the tessellator and the -/// table agree. Differs from the symbol-scale assets.analysePattern uses for the -/// client linestyles.json. -pub const LINESTYLE_PX_PER_MM = ls_px_per_mm; - -pub const LsSym = struct { name: []const u8, offset_px: f64 }; -pub const LsInfo = struct { - period_px: f64, - on_runs: []const [2]f64, // [lo,hi] screen px from period start - symbols: []const LsSym, - color_token: []const u8, - width_px: f64, -}; - -// Set once by the baker before tile generation; read-only during the parallel bake -// (encodeTile only reads), so it needs no lock. Absent => named lines fall -// back to the generic dashed stroke (live/host path, no regression). -var g_linestyles: std.StringHashMapUnmanaged(LsInfo) = .{}; - -/// Register one analysed complex linestyle (id = LineStyles file stem). `id` and the -/// LsInfo slices must outlive the bake (embedded XML / the bake's long-lived alloc). -pub fn registerLinestyle(gpa: Allocator, id: []const u8, info: LsInfo) void { - g_linestyles.put(gpa, id, info) catch {}; -} - -/// Drop all registered linestyles (host/test reset). -pub fn clearLinestyles(gpa: Allocator) void { - g_linestyles.deinit(gpa); - g_linestyles = .{}; -} - -/// Populate the complex-linestyle table from S-101 LineStyles XML sources -/// (id = file stem). IDEMPOTENT — a populated table is left untouched, so -/// every scene entry point (bake, lib renderView, CLI render) can call it -/// unconditionally; forgetting it silently degrades named linestyles -/// (MARSYS51, cables, pipelines, …) to generic dashed strokes. Mirrors the -/// Go lsInfoFromCatalog: mm geometry at the PresLib feature scale, S-52 -/// minimum pen width. `gpa` + `srcs` must outlive all tile generation. -pub fn registerLinestylesXml(gpa: Allocator, srcs: []const assets.LineStyleSrc) void { - if (g_linestyles.count() > 0) return; - const px = LINESTYLE_PX_PER_MM; - for (srcs) |s| { - const parsed = assets.parseLineStyle(gpa, s.xml) catch continue; - const period = parsed.interval_length * px; - if (period < 0.5) continue; // no interval to tile (pure-symbol style) - var runs = std.ArrayList([2]f64).empty; - for (parsed.dashes) |d| { - const lo = d.start * px; - const hi = (d.start + d.length) * px; - if (hi - lo > 1e-6) runs.append(gpa, .{ lo, hi }) catch {}; - } - var syms = std.ArrayList(LsSym).empty; - for (parsed.symbols) |sym| syms.append(gpa, .{ .name = sym.reference, .offset_px = sym.position * px }) catch {}; - var width = parsed.pen_width * px; - if (width < 0.6) width = 0.9; // S-52 minimum pen - registerLinestyle(gpa, s.id, .{ - .period_px = period, - .on_runs = runs.items, - .symbols = syms.items, - .color_token = parsed.pen_color, - .width_px = width, - }); - } -} - -const LsTangent = struct { p: tile.FPoint, dx: f64, dy: f64 }; - -/// Point at local arc `d` along rp plus the (un-normalised) tangent of its segment. -fn lsPointAndTangent(rp: []const tile.FPoint, rarc: []const f64, d_in: f64) ?LsTangent { - const total = rarc[rarc.len - 1]; - const d = std.math.clamp(d_in, 0, total); - var i: usize = 0; - while (i + 1 < rp.len) : (i += 1) { - if (d <= rarc[i + 1] or i + 2 == rp.len) { - const seg = rarc[i + 1] - rarc[i]; - const t: f64 = if (seg > 1e-12) (d - rarc[i]) / seg else 0; - return .{ - .p = .{ .x = rp[i].x + t * (rp[i + 1].x - rp[i].x), .y = rp[i].y + t * (rp[i + 1].y - rp[i].y) }, - .dx = rp[i + 1].x - rp[i].x, - .dy = rp[i + 1].y - rp[i].y, - }; - } - } - return null; -} - -fn lsLerpArc(rp: []const tile.FPoint, rarc: []const f64, d: f64) tile.FPoint { - return (lsPointAndTangent(rp, rarc, d) orelse LsTangent{ .p = rp[0], .dx = 0, .dy = 0 }).p; -} - -/// Sub-polyline of rp between local arc distances d0..d1 (endpoints interpolated). -fn lsSubPathByArc(a: Allocator, rp: []const tile.FPoint, rarc: []const f64, d0_in: f64, d1_in: f64) ![]tile.FPoint { - const total = rarc[rarc.len - 1]; - const d0 = std.math.clamp(d0_in, 0, total); - const d1 = std.math.clamp(d1_in, 0, total); - if (d1 - d0 < 1e-9) return &.{}; - var out = std.ArrayList(tile.FPoint).empty; - try out.append(a, lsLerpArc(rp, rarc, d0)); - for (rp, 0..) |p, i| { - if (rarc[i] > d0 and rarc[i] < d1) try out.append(a, p); - } - try out.append(a, lsLerpArc(rp, rarc, d1)); - return out.items; -} - -/// 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. -/// 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 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; - const fpts = try a.alloc(tile.FPoint, part.len); - for (part, 0..) |pt, i| fpts[i] = tile.worldToTileF(tile.lonLatToWorld(pt.lon(), pt.lat()), z, x, y, tile.EXTENT); - const arc = try a.alloc(f64, part.len); - 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| { - 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); - } - } - } - try surf.endFeature(); -} - /// Native S-52 fallback for SweptArea (SWPARE, objl 134). The S-101 Portrayal /// Catalogue ships no SweptArea rule (an IHO gap), so the Lua engine emits /// nothing for it. Mirror the Go reference's sweptAreaBuild: a dashed CHGRD @@ -2369,7 +1637,7 @@ fn emitSweptAreaFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize if (!overlaps(geomBounds(gp), tb)) continue; const proj = try a.alloc(mvt.Point, gp.len); for (gp, 0..) |pt, i| proj[i] = tile.project(pt.lon(), pt.lat(), z, x, y, tile.EXTENT); - var sub = try clipSimplifyLine(a, proj, box); + var sub = try tile.clipSimplifyLine(a, proj, box); if (opts.cover_clip) |cc| sub = clipRunsOutsideCover(a, sub, cc); if (sub.len == 0) continue; const parts = try a.alloc([]const mvt.Point, sub.len); @@ -2399,14 +1667,21 @@ fn emitSweptAreaFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize /// limit: stroke each ring with the MARSYS51 complex linestyle — the dashed "—A——B—" /// pattern carrying the EMMARS01 (IALA-A) and EMMARS02 (IALA-B) letter symbols — or /// NAVARE51 when ORIENT marks a direction of buoyage. The S-101 catalogue routes -/// M_NSYS to nothing (s101_adapt excludes it so this rule owns it), so without this +/// M_NSYS to nothing (the S-101 adapter excludes it so this rule owns it), so without this /// the boundary draws nothing. DrawingPriority 12. NOTE: the ORIENT-only DIRBOY /// direction-of-buoyage arrow (DIRBOY01/A1/B1, CentreOnArea) is not yet ported — /// absent on the reference data; the A/B boundary line is the visible feature. fn emitNavSystemFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { if (opts.suppress_lines) return; - const geo_parts = featureParts(a, cell, geo, fi, f) catch return; - if (geo_parts.len == 0) return; + // S-52 §8.6.2 boundary masking, mirrored from the portrayal stroke path: the + // producer flags the M_NSYS edges that coincide with the cell limit (MASK/USAG), + // so the system boundary strokes only where a REAL division runs — not along + // every cell junction of a uniform IALA region. The fill is unaffected. + const masked_boundary = cell.needsDrawableBoundary(f); + const geo_parts = if (masked_boundary) + cell.drawableLineParts(a, f) catch return + else + featureParts(a, cell, geo, fi, f) catch return; const fmeta = rs.FeatureMeta{ .draw_prio = 12, @@ -2421,12 +1696,17 @@ fn emitNavSystemFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize // IALA A/B system boundary (MARSYS51). Both stroke in CHGRD. const boundary: []const u8 = if (f.attr(s57.ATTR_ORIENT) != null) "NAVARE51" else "MARSYS51"; + // The masked complement (cell-limit stretches) rides along tagged, for the + // meta-bounds view; the standard display filters the class out anyway. + if (masked_boundary) try emitMaskedBoundary(a, cell, f, fmeta, z, x, y, tb, box, surf); + if (geo_parts.len == 0) return; + // Best-available: cut the covered stretches before tessellating/stroking. const nav_parts = if (opts.cover_clip) |cc| clipGeoPartsOutsideCover(a, geo_parts, cc, z, x, y) else geo_parts; // Tessellate the registered complex linestyle (dashes + the A/B letter symbols). - if (g_linestyles.get(boundary)) |info| { - try emitComplexLine(a, nav_parts, info, boundary, "CHGRD", !opts.suppress_points, z, x, y, box, &fmeta, surf); + if (linestyle.lookup(boundary)) |info| { + try linestyle.drawComplexLine(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. @@ -2436,7 +1716,39 @@ fn emitNavSystemFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize if (!overlaps(geomBounds(gp), tb)) continue; const proj = try a.alloc(mvt.Point, gp.len); for (gp, 0..) |pt, i| proj[i] = tile.project(pt.lon(), pt.lat(), z, x, y, tile.EXTENT); - const sub = try clipSimplifyLine(a, proj, box); + const sub = try tile.clipSimplifyLine(a, proj, box); + if (sub.len == 0) continue; + const parts = try a.alloc([]const mvt.Point, sub.len); + for (sub, 0..) |s, i| parts[i] = s; + try surf.strokeLine("CHGRD", 1, .dashed, parts, null); + } + try surf.endFeature(); +} + +/// The §8.6.2-masked complement of a boundary, baked as a `masked`-tagged plain +/// dashed line so the meta-bounds inspection view can OUTLINE the meta object — +/// a whole-cell M_NSYS has its entire boundary flagged as the cell limit, and +/// masking alone leaves the toggle nothing to show. The standard display never +/// renders it (the meta classes are filtered out unless meta-bounds is on), and +/// the tag keeps the masked pieces letter-free in the styled view too. +fn emitMaskedBoundary(a: Allocator, cell: s57.Cell, f: s57.Feature, base: rs.FeatureMeta, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, surf: rs.Surface) !void { + const masked_parts = cell.maskedLineParts(a, f) catch return; + if (masked_parts.len == 0) return; + var fmeta = base; + fmeta.masked = true; + // NOT cover-clipped: this is the meta-bounds inspection outline of one chart's + // OWN extent, so it must trace that chart's whole boundary even where a finer + // chart owns the ground on top of it. Clipping it to the best-available cover + // (as the drawn portrayal is) carves the outline apart wherever a finer chart + // overlaps — a coarse chart's rectangle comes out in disjoint fragments. The + // inspection view wants every contributing chart's complete extent. + try surf.beginFeature(&fmeta); + for (masked_parts) |gp| { + if (gp.len < 2) continue; + if (!overlaps(geomBounds(gp), tb)) continue; + const proj = try a.alloc(mvt.Point, gp.len); + for (gp, 0..) |pt, i| proj[i] = tile.project(pt.lon(), pt.lat(), z, x, y, tile.EXTENT); + const sub = try tile.clipSimplifyLine(a, proj, box); if (sub.len == 0) continue; const parts = try a.alloc([]const mvt.Point, sub.len); for (sub, 0..) |s, i| parts[i] = s; @@ -2456,7 +1768,12 @@ fn emitNavSystemFallback(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize fn emitDashedBoundary(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, color: []const u8, width: f64, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { if (f.prim != 2 and f.prim != 3) return; if (opts.suppress_lines) return; // coarse band over finer M_COVR (centre): drop the stroke - const geo_parts = featureParts(a, cell, geo, fi, f) catch return; + // Same S-52 §8.6.2 boundary masking as the portrayal stroke path (see + // emitNavSystemFallback): masked cell-limit edges don't stroke. + const geo_parts = if (cell.needsDrawableBoundary(f)) + cell.drawableLineParts(a, f) catch return + else + featureParts(a, cell, geo, fi, f) catch return; if (geo_parts.len == 0) return; const fmeta = rs.FeatureMeta{ @@ -2473,7 +1790,7 @@ fn emitDashedBoundary(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, g if (!overlaps(geomBounds(gp), tb)) continue; const proj = try a.alloc(mvt.Point, gp.len); for (gp, 0..) |pt, i| proj[i] = tile.project(pt.lon(), pt.lat(), z, x, y, tile.EXTENT); - var sub = try clipSimplifyLine(a, proj, box); + var sub = try tile.clipSimplifyLine(a, proj, box); if (opts.cover_clip) |cc| sub = clipRunsOutsideCover(a, sub, cc); if (sub.len == 0) continue; const parts = try a.alloc([]const mvt.Point, sub.len); @@ -2499,7 +1816,7 @@ fn isCoverageFeature(f: s57.Feature) bool { /// CellOpts.oscl). The style shows it only while the display is grossly overscale /// (denominator FINER/smaller than oscl, i.e. X2+) and paints it ABOVE overscaled /// cells' fills but BELOW at-scale (finer) cells' fills — the occlusion trick that -/// leaves the hatch only on coarse-only patches (specs/overscale.md). +/// leaves the hatch only on coarse-only patches. fn emitOverscaleHatch(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, geo: ?GeoParts, z: u8, x: u32, y: u32, tb: [4]f64, box: tile.Box, opts: CellOpts, surf: rs.Surface) !void { const geo_parts = featureParts(a, cell, geo, fi, f) catch return; if (geo_parts.len == 0) return; @@ -2513,7 +1830,7 @@ fn emitOverscaleHatch(a: Allocator, cell: s57.Cell, f: s57.Feature, fi: usize, g if (ring.len >= 3) try rings.append(a, ring); } if (rings.items.len == 0) return; - const parts = try orientAreaRings(a, rings.items); + const parts = try mvt.orientAreaRings(a, rings.items); const fmeta = rs.FeatureMeta{ // S-52 §10.1.10.2: the overscale pattern draws at display priority 3 in // DISPLAY BASE (the indication is never optional content). No class/pick @@ -2796,10 +2113,40 @@ fn appendCellFeatures( // LIGHTS cull margin: display-mm sector figures reach ~1 tile at every zoom; // ground-length legs (directional lights) reach their honest per-zoom span. const light_reach = lightReachTiles(opts.light_range_m, z, (tb[1] + tb[3]) * 0.5); + // Sub-band SCAMIN cull: below the cell's native band floor this is a fill-up + // tile (the compositor pulls finer data into coarser zooms where nothing + // coarser covers), and a feature whose (floored) SCAMIN cannot display + // anywhere in this tile is dead weight — cull it so the z4 copy of a harbor + // cell carries land/coast/majors, not every sounding. The threshold is the + // tile's most permissive display denominator (its highest-|lat| edge) with + // one zoom of slack, so the cull stays strictly looser than any client + // gate-latitude choice; SCAMIN-less features always pass. + const subband_floor = @import("tiles").band.bandZooms(@import("tiles").band.bandOf(cell.params.cscl)).min; + const subband_min_denom: f64 = if (z < subband_floor) blk: { + const max_abs_lat = @max(@abs(tb[1]), @abs(tb[3])); + const k = @import("style").scaminGateK(max_abs_lat); + break :blk k / @as(f64, @floatFromInt(@as(u64, 1) << @intCast(z))) / 2.0; + } else 0; for (cell.features, 0..) |f, fi| { // Isolated single-feature render (explore --kitty thumbnail): draw only // the requested feature, skip the rest of the cell. if (opts.only_fi) |only| if (fi != only) continue; + if (subband_min_denom > 0) { + if (effScamin(f, opts)) |sc| { + if (@as(f64, @floatFromInt(sc)) < subband_min_denom) continue; + } else if (f.objl == 75) { + // LIGHTS is the one class whose SCAMIN-less features do NOT ride + // sub-band: producers leave SCAMIN off most fine-band lights (cell + // selection is the intended gate — an ECDIS at this scale would + // never load the cell), and a light's portrayal is all fixed + // display-size construction — flare, characteristic text, 20/25 mm + // sector legs and arcs — which reads as a continent-sized doodle + // on a fill-up tile. The true small-scale lights arrive from the + // overview/general cells, SCAMIN-carrying. Ground features + // (land/coast/depth) keep riding. + continue; + } + } var ml = mlon; var mt = mlat; if (f.objl == 75) { @@ -2838,8 +2185,8 @@ fn appendCellFeatures( if (!fopts.suppress_points and hasAdditionalInfo(f)) { try emitCentredSymbol(a, cell.*, f, fi, geo, "INFORM01", 8, 2, z, x, y, tb, fopts, surf); } - // S-52 §10.1.10.2 overscale area at a chart scale boundary - // (specs/overscale.md): a cell's M_COVR (CATCOV=1) coverage polygon rides + // S-52 §10.1.10.2 overscale area at a chart scale boundary: + // a cell's M_COVR (CATCOV=1) coverage polygon rides // the tile as an AP(OVERSC01) hatch gated on `oscl` (X2) ONLY when // overscale_hatch is set — a strictly-finer-CSCL cell rides the tile (a // scale boundary) AND this cell wins the pure quilt somewhere (it is the @@ -2866,7 +2213,7 @@ fn appendCellFeatures( continue; } if (f.objl == 163) { - if (try buildSyminsPortrayal(a, f)) |sp| { + if (try symins.buildSyminsPortrayal(a, f)) |sp| { try processFeatureParsed(a, cell.*, f, fi, geo, geo_world, sp, 2, 2, z, x, y, tb, box, fopts, surf); continue; } @@ -2893,7 +2240,7 @@ fn appendCellFeatures( try emitNavSystemFallback(a, cell.*, f, fi, geo, z, x, y, tb, box, fopts, surf); continue; } - if (f.objl != s57.OBJL_TOPMAR and s101_adapt.resolveClass(f) == null) { + if (f.objl != s57.OBJL_TOPMAR and adapter.resolveClass(f) == null) { if (!fopts.suppress_points) try emitCentredSymbol(a, cell.*, f, fi, geo, "QUESMRK1", 6, 1, z, x, y, tb, fopts, surf); continue; } @@ -2918,7 +2265,7 @@ fn appendCellFeatures( if (ring.len >= 3) try rings.append(a, ring); } if (rings.items.len == 0) continue; - const parts = try orientAreaRings(a, rings.items); + const parts = try mvt.orientAreaRings(a, rings.items); var aprops = std.ArrayList(mvt.Prop).empty; try aprops.append(a, .{ .key = "class", .value = .{ .string = cls.name } }); try aprops.append(a, .{ .key = "color_token", .value = .{ .string = cls.color } }); @@ -2936,7 +2283,7 @@ fn appendCellFeatures( if (!overlaps(geomBounds(gp), tb)) continue; const proj = try a.alloc(mvt.Point, gp.len); for (gp, 0..) |p, i| proj[i] = tile.project(p.lon(), p.lat(), z, x, y, tile.EXTENT); - var sub = try clipSimplifyLine(a, proj, box); + var sub = try tile.clipSimplifyLine(a, proj, box); if (fopts.cover_clip) |cc| sub = clipRunsOutsideCover(a, sub, cc); if (sub.len == 0) continue; const parts = try a.alloc([]const mvt.Point, sub.len); @@ -3065,26 +2412,17 @@ test "appendTextProps: halo (CHWHT/1) gated on font_size >= 10, matching the ora } } -test "shortenName: buoy/light names reduce to the QUOTED chart designation" { - var arena_s = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena_s.deinit(); - const al = arena_s.allocator(); - // Verbose NOAA OBJNAM tagged "by " by the S-101 buoy rules -> the - // designation IN QUOTES (paper-chart convention: not misread as a sounding). - try std.testing.expectEqualStrings("\"78A\"", try shortenName(al, "by Chesapeake Channel Lighted Buoy 78A")); - try std.testing.expectEqualStrings("\"CR\"", try shortenName(al, "by Chesapeake Channel Lighted Buoy CR")); - try std.testing.expectEqualStrings("\"3\"", try shortenName(al, "by Tangier Sound Daybeacon 3")); - try std.testing.expectEqualStrings("\"2CR\"", try shortenName(al, "by Lighted Whistle Buoy 2CR")); - // No type-word -> the stripped name (already short), unquoted. - try std.testing.expectEqualStrings("22", try shortenName(al, "by 22")); - // No "by " prefix -> passthrough (depth label, light elevation, place name). - try std.testing.expectEqualStrings(" 4.6m", try shortenName(al, " 4.6m")); - try std.testing.expectEqualStrings("Herring Bay", try shortenName(al, "Herring Bay")); - // "Light"/"Lt" are NOT split words -> a named light keeps its name (defensive; such - // labels don't carry the "by " buoy prefix anyway). - try std.testing.expectEqualStrings("Thomas Point Light", try shortenName(al, "by Thomas Point Light")); - // Beacon names ("bn " prefix) reduce the same way. - try std.testing.expectEqualStrings("\"2\"", try shortenName(al, "bn Turn Rock Daybeacon 2")); +test "stripNameTag: strips the by/bn aid-name tag, keeps the full name" { + // The S-101 buoy/beacon rules tag OBJNAM "by "/"bn "; the tag is + // stripped and the FULL name shown (the designation-shortening was removed). + try std.testing.expectEqualStrings("Chesapeake Channel Lighted Buoy 78A", stripNameTag("by Chesapeake Channel Lighted Buoy 78A")); + try std.testing.expectEqualStrings("Tangier Sound Daybeacon 3", stripNameTag("by Tangier Sound Daybeacon 3")); + try std.testing.expectEqualStrings("Turn Rock Daybeacon 2", stripNameTag("bn Turn Rock Daybeacon 2")); + try std.testing.expectEqualStrings("22", stripNameTag("by 22")); + // No "by "/"bn " prefix -> passthrough (depth label, light elevation, place name). + try std.testing.expectEqualStrings(" 4.6m", stripNameTag(" 4.6m")); + try std.testing.expectEqualStrings("Herring Bay", stripNameTag("Herring Bay")); + try std.testing.expectEqualStrings("Thomas Point Light", stripNameTag("by Thomas Point Light")); } test "listHasAny splits S-57 comma lists and matches any target" { @@ -3095,54 +2433,6 @@ test "listHasAny splits S-57 comma lists and matches any target" { try std.testing.expect(!listHasAny("", &.{18})); } -test "orientAreaRings subtracts a hole: exterior CW (+), interior CCW (-)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const a = arena.allocator(); - - // A sea-area exterior square (CCW as authored) with a smaller island hole - // inside it (also CCW as authored). y is down in tile space. - const ext = [_]mvt.Point{ - .{ .x = 0, .y = 0 }, .{ .x = 0, .y = 100 }, - .{ .x = 100, .y = 100 }, .{ .x = 100, .y = 0 }, - }; - const hole = [_]mvt.Point{ - .{ .x = 40, .y = 40 }, .{ .x = 40, .y = 60 }, - .{ .x = 60, .y = 60 }, .{ .x = 60, .y = 40 }, - }; - // Pass the hole first to prove ordering is by geometry, not input order. - const rings = [_][]const mvt.Point{ hole[0..], ext[0..] }; - const out = try orientAreaRings(a, &rings); - - try std.testing.expectEqual(@as(usize, 2), out.len); - // First emitted ring is the exterior (positive signed area), then its hole - // (negative). This is the winding MapLibre reads to cut the hole out. - try std.testing.expect(ringSignedArea(out[0]) > 0); - try std.testing.expect(ringSignedArea(out[1]) < 0); - // The exterior must be the 100x100 ring, the hole the 20x20 one. - try std.testing.expect(@abs(ringSignedArea(out[0])) > @abs(ringSignedArea(out[1]))); -} - -test "orientAreaRings keeps disjoint parts as separate exteriors (multipolygon)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const a = arena.allocator(); - - // Two disjoint squares (CTNARE-style multi-part area): both are exteriors, - // both wound positive, neither becomes a hole of the other. - const r0 = [_]mvt.Point{ - .{ .x = 0, .y = 0 }, .{ .x = 0, .y = 10 }, .{ .x = 10, .y = 10 }, .{ .x = 10, .y = 0 }, - }; - const r1 = [_]mvt.Point{ - .{ .x = 50, .y = 50 }, .{ .x = 50, .y = 60 }, .{ .x = 60, .y = 60 }, .{ .x = 60, .y = 50 }, - }; - const rings = [_][]const mvt.Point{ r0[0..], r1[0..] }; - const out = try orientAreaRings(a, &rings); - try std.testing.expectEqual(@as(usize, 2), out.len); - try std.testing.expect(ringSignedArea(out[0]) > 0); - try std.testing.expect(ringSignedArea(out[1]) > 0); -} - fn findProp(props: []const mvt.Prop, key: []const u8) ?mvt.Value { for (props) |pr| if (std.mem.eql(u8, pr.key, key)) return pr.value; return null; @@ -3157,87 +2447,6 @@ test "featureScamin reads s57 attr 133" { try std.testing.expectEqual(@as(?i64, null), featureScamin(without)); } -test "collectLightReach: AugmentedRay ground legs + anchors from the streams" { - const gpa = std.testing.allocator; - const feats = [_]s57.Feature{ - // A directional light: GeographicCRS leg 16668 m + a sector arc. - .{ .rcnm = 100, .rcid = 1, .prim = 1, .objl = 75, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 1 }, .ornt = 255 }} }, - // A plain sectored light: LocalCRS (display-mm) legs only. - .{ .rcnm = 100, .rcid = 2, .prim = 1, .objl = 75, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 2 }, .ornt = 255 }} }, - // A buoy with no figures at all. - .{ .rcnm = 100, .rcid = 3, .prim = 1, .objl = 14, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 3 }, .ornt = 255 }} }, - }; - var cell = s57.Cell{ - .params = .{ .cscl = 80_000 }, - .vectors = &.{}, - .features = &feats, - .nodes = std.AutoHashMap(u64, s57.LonLat).init(gpa), - .edges = std.AutoHashMap(u32, usize).init(gpa), - .sounding_vecs = std.AutoHashMap(u64, usize).init(gpa), - .arena = std.heap.ArenaAllocator.init(gpa), - }; - defer cell.deinit(); - try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 1, s57.LonLat.init(-76.52, 39.20)); - try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 2, s57.LonLat.init(-76.40, 39.30)); - try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 3, s57.LonLat.init(-76.10, 39.10)); - - const streams = [_]?[]const u8{ - "ViewingGroup:27070;DrawingPriority:8;AugmentedRay:GeographicCRS,45.0,GeographicCRS,16668;LineStyle:dash,3.51,0.32,CHBLK;LineInstruction:_simple_;ClearGeometry", - "DrawingPriority:8;AugmentedRay:GeographicCRS,120.0,LocalCRS,25.0;LineInstruction:_simple_;ArcByRadius:0,0,20,120,90;LineInstruction:_simple_;ClearGeometry", - "DrawingPriority:7;PointInstruction:BOYLAT01", - }; - const r = collectLightReach(&cell, &streams); - // Ground range from the directional leg only; mm-only figures add no range. - try std.testing.expectEqual(@as(f64, 16668), r.range_m); - // The bbox unions BOTH figure-bearing lights, not the figuresless buoy. - const bb = r.bbox orelse return error.TestUnexpectedResult; - try std.testing.expectApproxEqAbs(@as(f64, -76.52), bb[0], 1e-9); - try std.testing.expectApproxEqAbs(@as(f64, 39.20), bb[1], 1e-9); - try std.testing.expectApproxEqAbs(@as(f64, -76.40), bb[2], 1e-9); - try std.testing.expectApproxEqAbs(@as(f64, 39.30), bb[3], 1e-9); - - // No streams at all -> no reach. - try std.testing.expectEqual(@as(?[4]f64, null), collectLightReach(&cell, null).bbox); - - // An explicit AugmentedPoint anchor extends the bbox beyond the node. - const anchored = [_]?[]const u8{ - "AugmentedPoint:GeographicCRS,-76.60,39.10;ArcByRadius:0,0,25,0,360;LineInstruction:_simple_;ClearGeometry", - null, - null, - }; - const ra = collectLightReach(&cell, &anchored); - const ab = ra.bbox orelse return error.TestUnexpectedResult; - try std.testing.expectApproxEqAbs(@as(f64, -76.60), ab[0], 1e-9); - try std.testing.expectApproxEqAbs(@as(f64, 39.10), ab[1], 1e-9); -} - -test "scanLightReachAttrs: sectored/directional attrs drive the conservative reach" { - const gpa = std.testing.allocator; - const sect_attrs = [_]s57.Attr{ .{ .code = s57.ATTR_SECTR1, .value = "45" }, .{ .code = s57.ATTR_SECTR2, .value = "90" } }; - const dir_attrs = [_]s57.Attr{ .{ .code = s57.ATTR_CATLIT, .value = "1" }, .{ .code = s57.ATTR_ORIENT, .value = "195" }, .{ .code = s57.ATTR_VALNMR, .value = "12" } }; - const feats = [_]s57.Feature{ - .{ .rcnm = 100, .rcid = 1, .prim = 1, .objl = 75, .attrs = §_attrs, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 1 }, .ornt = 255 }} }, - .{ .rcnm = 100, .rcid = 2, .prim = 1, .objl = 75, .attrs = &dir_attrs, .refs = &.{.{ .name = .{ .rcnm = s57.RCNM_VI, .rcid = 2 }, .ornt = 255 }} }, - }; - var cell = s57.Cell{ - .params = .{ .cscl = 80_000 }, - .vectors = &.{}, - .features = &feats, - .nodes = std.AutoHashMap(u64, s57.LonLat).init(gpa), - .edges = std.AutoHashMap(u32, usize).init(gpa), - .sounding_vecs = std.AutoHashMap(u64, usize).init(gpa), - .arena = std.heap.ArenaAllocator.init(gpa), - }; - defer cell.deinit(); - try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 1, s57.LonLat.init(-76.52, 39.20)); - try cell.nodes.put((@as(u64, s57.RCNM_VI) << 32) | 2, s57.LonLat.init(-76.40, 39.30)); - - const r = scanLightReachAttrs(&cell); - try std.testing.expect(r.bbox != null); - // Directional CATLIT 1 + ORIENT: nmi2metres(VALNMR 12) ground leg. - try std.testing.expectEqual(@as(f64, 12.0 * 1852.0), r.range_m); -} - test "processFeatureInstr routes SCAMIN point to the bucket + carries draw_prio/scamin" { const gpa = std.testing.allocator; var arena = std.heap.ArenaAllocator.init(gpa); @@ -3420,51 +2629,6 @@ test "generate a tile from a cell is well-formed MVT" { try std.testing.expectEqual(@as(usize, 0), out.len); } -test "buildSyminsPortrayal parses SY/TX/LS/LC/AC/AP" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const a = arena.allocator(); - - const attrs = [_]s57.Attr{.{ - .code = SYMINS_ATTR, - .value = "SY(BOYSPP01,45);TX('Hello',1,2,0,'15110',0,0,CHRED,28);" ++ - "LS(DASH,3,CHGRD);LS(SOLD,2,CHBLK);LC(NAVARE51);AC(DEPVS);AP(DIAMOND1)", - }}; - const f = s57.Feature{ .rcnm = 100, .rcid = 1, .prim = 3, .objl = 163, .attrs = &attrs }; - - const p = (try buildSyminsPortrayal(a, f)) orelse return error.NoPortrayal; - - try std.testing.expectEqual(@as(usize, 1), p.points.len); - try std.testing.expectEqualStrings("BOYSPP01", p.points[0].symbol); - try std.testing.expectEqual(@as(f64, 45), p.points[0].rotation); - - try std.testing.expectEqual(@as(usize, 1), p.texts.len); - try std.testing.expectEqualStrings("Hello", p.texts[0].text); - try std.testing.expectEqualStrings("CHRED", p.texts[0].color); - try std.testing.expectEqual(@as(i64, 28), p.texts[0].group); - - try std.testing.expectEqual(@as(usize, 3), p.lines.len); // 2x LS + 1x LC - try std.testing.expectEqualStrings("dash", p.lines[0].style); // DASH -> dashed - try std.testing.expectEqual(@as(f64, 3), p.lines[0].width); - try std.testing.expectEqualStrings("CHGRD", p.lines[0].color); - try std.testing.expectEqualStrings("solid", p.lines[1].style); // SOLD -> solid - try std.testing.expectEqualStrings("NAVARE51", p.lines[2].style); // LC name verbatim - - try std.testing.expectEqualStrings("DEPVS", p.fill_token.?); - try std.testing.expectEqual(@as(usize, 1), p.patterns.len); - try std.testing.expectEqualStrings("DIAMOND1", p.patterns[0]); - - // A blank / absent SYMINS yields no portrayal. - const f_empty = s57.Feature{ .rcnm = 100, .rcid = 2, .prim = 3, .objl = 163, .attrs = &[_]s57.Attr{.{ .code = SYMINS_ATTR, .value = " " }} }; - try std.testing.expect((try buildSyminsPortrayal(a, f_empty)) == null); - - // Instruction-splitting honours a ';' inside a quoted TX string. - const f_semi = s57.Feature{ .rcnm = 100, .rcid = 3, .prim = 1, .objl = 163, .attrs = &[_]s57.Attr{.{ .code = SYMINS_ATTR, .value = "TX('a;b',1,2,0,'15110',0,0,CHBLK,28)" }} }; - const ps = (try buildSyminsPortrayal(a, f_semi)) orelse return error.NoPortrayal; - try std.testing.expectEqual(@as(usize, 1), ps.texts.len); - try std.testing.expectEqualStrings("a;b", ps.texts[0].text); -} - test "QUASOU=5 no-bottom sounding draws the low-accuracy ring (S-65 DepthNoBottomFound)" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -3558,157 +2722,8 @@ test "DANGER01/02 on a VALSOU danger normalizes + tags danger_depth/sym_deep for test { _ = bake_enc; -} - -// ---- bundle-sourced replay (baked tile -> Surface calls) -------------------- -// -// The tile schema is a serialized Surface-call stream, so a baked tile can be -// replayed onto any Surface — the substrate for rendering PMTiles bundles to -// pixels with no source cells. LOSSY by design: the bake-time portrayal -// context is frozen (SafetyContour/Depth 30); the live-swappable props the -// tile path bakes (danger_depth/sym_deep, sym_s/sym_g depth + quality-ring -// prefixes) are re-expanded here, so the mariner's danger swap, sounding -// bold/faint split, and display unit still evaluate LIVE. - -fn propOf(props: []const mvt.Prop, key: []const u8) ?mvt.Value { - for (props) |p| if (std.mem.eql(u8, p.key, key)) return p.value; - return null; -} - -fn propInt(props: []const mvt.Prop, key: []const u8, default: i64) i64 { - const v = propOf(props, key) orelse return default; - return switch (v) { - .int => |i| i, - .uint => |u| @intCast(u), - .double => |d| @intFromFloat(d), - .float => |f| @intFromFloat(f), - else => default, - }; -} - -fn propF64(props: []const mvt.Prop, key: []const u8) ?f64 { - const v = propOf(props, key) orelse return null; - return switch (v) { - .double => |d| d, - .float => |f| f, - .int => |i| @floatFromInt(i), - .uint => |u| @floatFromInt(u), - else => null, - }; -} - -fn propStr(props: []const mvt.Prop, key: []const u8) []const u8 { - const v = propOf(props, key) orelse return ""; - return switch (v) { - .string => |s| s, - else => "", - }; -} - -fn metaFromProps(props: []const mvt.Prop) rs.FeatureMeta { - return .{ - .draw_prio = propInt(props, "draw_prio", 0), - .cat = propInt(props, "cat", 1), - .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), - .date_start = propStr(props, "date_start"), - .date_end = propStr(props, "date_end"), - }; -} - -/// 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(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"); - const is_lines = std.mem.startsWith(u8, layer.name, "lines"); - const is_points = std.mem.startsWith(u8, layer.name, "point_symbols"); - const is_soundings = std.mem.eql(u8, layer.name, "soundings"); - const is_text = std.mem.startsWith(u8, layer.name, "text"); - for (layer.features) |f| { - const meta = metaFromProps(f.properties); - try surf.beginFeature(&meta); - defer surf.endFeature() catch {}; - if (is_patterns) { - try surf.fillPattern(propStr(f.properties, "pattern_name"), f.parts); - } else if (is_areas) { - const d1 = propF64(f.properties, "drval1"); - 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) { - if (f.parts.len == 0 or f.parts[0].len == 0) continue; - try surf.drawSymbol( - propStr(f.properties, "symbol_name"), - f.parts[0][0], - propF64(f.properties, "rotation_deg") orelse 0, - propF64(f.properties, "scale") orelse SYMBOL_SCALE, - propInt(f.properties, "rot_north", 0) != 0, - .point, - propF64(f.properties, "danger_depth"), // live swap re-evaluates - ); - } else if (is_soundings) { - if (f.parts.len == 0 or f.parts[0].len == 0) continue; - const depth = propF64(f.properties, "depth") orelse continue; - // The quality-ring flags are encoded in the baked glyph list's - // leading tokens (SNDFRM04 B1 swept / C2/C3 low-accuracy) — - // recover them so the live recomposition keeps the rings. - const sym_s = propStr(f.properties, "sym_s"); - const swept = std.mem.indexOf(u8, sym_s, "SB1") != null; - const low_acc = std.mem.indexOf(u8, sym_s, "SC3") != null; - try surf.drawSounding(depth, swept, low_acc, f.parts[0][0]); - } else if (is_text) { - if (f.parts.len == 0 or f.parts[0].len == 0) continue; - var ox: f64 = 0; - var oy: f64 = 0; - const loff = propStr(f.properties, "loff"); - if (loff.len > 0) { - var it = std.mem.splitScalar(u8, loff, ','); - const TEXT_BODY_MM = 3.51; - ox = (std.fmt.parseFloat(f64, it.next() orelse "0") catch 0) * TEXT_BODY_MM; - oy = (std.fmt.parseFloat(f64, it.next() orelse "0") catch 0) * TEXT_BODY_MM; - } - const style = rs.TextStyle{ - .color = propStr(f.properties, "color_token"), - .font_size = propF64(f.properties, "font_size_px") orelse 12, - .halign = propStr(f.properties, "halign"), - .valign = propStr(f.properties, "valign"), - .offset_x = ox, - .offset_y = oy, - .group = propInt(f.properties, "tgrp", 0), - }; - try surf.drawText(propStr(f.properties, "text"), &style, f.parts[0][0]); - } - } - } + _ = symins; + _ = lightreach; + _ = linestyle; + _ = replay; } diff --git a/src/scene/symins.zig b/src/scene/symins.zig new file mode 100644 index 0000000..37acfd7 --- /dev/null +++ b/src/scene/symins.zig @@ -0,0 +1,324 @@ +//! SYMINS02 native fallback (S-52 PresLib §13.2.18 / §10.3.3.8): portray an S-57 +//! NEWOBJ from its producer SYMINS attribute (code 192) — a ';'-separated list of +//! S-52 draw ops SY()/TX()/TE()/LS()/LC()/AC()/AP() rendered verbatim, instead of +//! the S-101 V-AIS alias the FeatureCatalogue maps NEWOBJ to. This is how the S-52 +//! PresLib "ECDIS Chart 1" labels / boundaries / fills are drawn. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const s57 = @import("s57"); +const instructions = @import("s101").instructions; +const catalogue = @import("s101").catalogue; +const style = @import("style"); + +pub const SYMINS_ATTR: u16 = 192; + +fn syminsTrimQuotes(s: []const u8) []const u8 { + return std.mem.trim(u8, s, "'\""); +} + +fn syminsArgAt(args: []const []const u8, i: usize) []const u8 { + return if (i < args.len) args[i] else ""; +} + +/// The S-57 attribute value referenced by acronym (e.g. "OBJNAM"), trimmed, or null +/// when absent/blank. Mirrors Go lookupAttributeText over the feature's attrs. +fn syminsFeatAttr(f: s57.Feature, acr: []const u8) ?[]const u8 { + for (f.attrs) |at| { + const a2 = catalogue.attrAcronym(at.code) orelse continue; + if (std.ascii.eqlIgnoreCase(a2, acr)) { + const v = std.mem.trim(u8, at.value, " "); + return if (v.len == 0) null else v; + } + } + return null; +} + +/// Split a SYMINS string on ';', honouring quotes and nested parens (so a ';' inside +/// TX('a;b',…) or between parens isn't a split). Returns slices into `s`. +fn syminsSplitInstructions(a: Allocator, s: []const u8) ![]const []const u8 { + var out = std.ArrayList([]const u8).empty; + var depth: i32 = 0; + var in_quote = false; + var start: usize = 0; + var i: usize = 0; + while (i < s.len) : (i += 1) { + switch (s[i]) { + '\'', '"' => in_quote = !in_quote, + '(' => if (!in_quote) { + depth += 1; + }, + ')' => if (!in_quote) { + depth -= 1; + }, + ';' => if (!in_quote and depth == 0) { + try out.append(a, s[start..i]); + start = i + 1; + }, + else => {}, + } + } + if (start < s.len) try out.append(a, s[start..]); + return out.items; +} + +/// Split "OP(params)" into the op and inner params, or null when malformed. +fn syminsSplitOp(instr0: []const u8) ?struct { op: []const u8, params: []const u8 } { + const instr = std.mem.trim(u8, instr0, " \t"); + const open = std.mem.indexOfScalar(u8, instr, '(') orelse return null; + const close = std.mem.lastIndexOfScalar(u8, instr, ')') orelse return null; + if (open == 0 or close < open) return null; + return .{ .op = std.mem.trim(u8, instr[0..open], " \t"), .params = instr[open + 1 .. close] }; +} + +/// Split an instruction's params on ',', honouring quotes. Returns trimmed slices. +fn syminsSplitArgs(a: Allocator, params: []const u8) ![]const []const u8 { + var out = std.ArrayList([]const u8).empty; + var in_quote = false; + var start: usize = 0; + var i: usize = 0; + while (i < params.len) : (i += 1) { + const c = params[i]; + if (c == '\'' or c == '"') { + in_quote = !in_quote; + } else if (c == ',' and !in_quote) { + try out.append(a, std.mem.trim(u8, params[start..i], " \t")); + start = i + 1; + } + } + try out.append(a, std.mem.trim(u8, params[start..], " \t")); + return out.items; +} + +/// printf-style format substitution for a SYMINS TE() instruction (S-52 §3.2.3.2): +/// each %-spec consumes the next attribute name and formats its value (floats honour +/// .precision; integer convs round; the '0' flag + width zero-pad). Mirrors Go +/// formatSubstitute + appendConverted + zeroPad. Returns null when an attribute is +/// missing (the whole label is then dropped, as in Go). +fn syminsZeroPad(a: Allocator, out: *std.ArrayList(u8), s: []const u8, width: usize, flags: []const u8) !void { + const has0 = std.mem.indexOfScalar(u8, flags, '0') != null; + const has_minus = std.mem.indexOfScalar(u8, flags, '-') != null; + if (width <= s.len or !has0 or has_minus) { + try out.appendSlice(a, s); + return; + } + const pad = width - s.len; + const signed = s.len > 0 and (s[0] == '-' or s[0] == '+' or s[0] == ' '); + if (signed) try out.append(a, s[0]); + var k: usize = 0; + while (k < pad) : (k += 1) try out.append(a, '0'); + try out.appendSlice(a, if (signed) s[1..] else s); +} + +fn syminsAppendConverted(a: Allocator, out: *std.ArrayList(u8), val: []const u8, conv: u8, precision: i32, width: usize, flags: []const u8) !void { + var buf: [512]u8 = undefined; + var s: []const u8 = val; + switch (conv) { + 'f', 'e', 'g' => { + if (std.fmt.parseFloat(f64, std.mem.trim(u8, val, " \t"))) |x| { + s = std.fmt.float.render(&buf, x, .{ + .mode = .decimal, + .precision = if (precision >= 0) @as(usize, @intCast(precision)) else null, + }) catch val; + } else |_| {} + }, + 'd', 'i', 'u', 'x' => { + if (std.fmt.parseFloat(f64, std.mem.trim(u8, val, " \t"))) |x| { + const r: i64 = @intFromFloat(@round(x)); + s = std.fmt.bufPrint(&buf, "{d}", .{r}) catch val; + } else |_| {} + }, + else => {}, + } + try syminsZeroPad(a, out, s, width, flags); +} + +fn syminsFormatSubstitute(a: Allocator, f: s57.Feature, format: []const u8, names: []const []const u8) !?[]const u8 { + var out = std.ArrayList(u8).empty; + var attr_idx: usize = 0; + var i: usize = 0; + while (i < format.len) { + if (format[i] != '%' or i + 1 >= format.len) { + try out.append(a, format[i]); + i += 1; + continue; + } + if (format[i + 1] == '%') { + try out.append(a, '%'); + i += 2; + continue; + } + var j = i + 1; + const flags_start = j; + while (j < format.len and std.mem.indexOfScalar(u8, "-+ #0", format[j]) != null) j += 1; + const flags = format[flags_start..j]; + var width: usize = 0; + while (j < format.len and format[j] >= '0' and format[j] <= '9') : (j += 1) width = width * 10 + (format[j] - '0'); + var precision: i32 = -1; + if (j < format.len and format[j] == '.') { + j += 1; + var p: i32 = 0; + while (j < format.len and format[j] >= '0' and format[j] <= '9') : (j += 1) p = p * 10 + @as(i32, format[j] - '0'); + precision = p; + } + while (j < format.len and (format[j] == 'l' or format[j] == 'h' or format[j] == 'L')) j += 1; + if (j >= format.len) { + try out.appendSlice(a, format[i..]); // malformed trailing spec -> literal + break; + } + const conv = format[j]; + switch (conv) { + 's', 'c', 'd', 'i', 'u', 'x', 'f', 'e', 'g' => { + if (attr_idx >= names.len) return null; + const acr = names[attr_idx]; + attr_idx += 1; + const val = syminsFeatAttr(f, acr) orelse return null; + try syminsAppendConverted(a, &out, val, conv, precision, width, flags); + }, + else => try out.appendSlice(a, format[i .. j + 1]), // unknown conversion -> literal + } + i = j + 1; + } + return out.items; +} + +/// Parse a SYMINS TX()/TE() instruction into a Text. The Text model carries the +/// label string, colour and viewing group; the S-52 justification / offset / font / +/// halo fields are dropped (tracked OpText findings), matching the current text path. +fn syminsText(a: Allocator, f: s57.Feature, op: []const u8, params: []const u8) !?instructions.Text { + const args = try syminsSplitArgs(a, params); + var text: []const u8 = ""; + var color_idx: usize = undefined; + var display_idx: usize = undefined; + if (std.mem.eql(u8, op, "TE")) { + if (args.len < 10) return null; + var names = std.ArrayList([]const u8).empty; + var it = std.mem.splitScalar(u8, syminsTrimQuotes(args[1]), ','); + while (it.next()) |nm| { + const t = std.mem.trim(u8, nm, " \t"); + if (t.len > 0) try names.append(a, t); + } + text = (try syminsFormatSubstitute(a, f, syminsTrimQuotes(args[0]), names.items)) orelse return null; + color_idx = 8; + display_idx = 9; + } else { // TX + if (args.len < 9) return null; + const raw = args[0]; + if (raw.len > 0 and (raw[0] == '\'' or raw[0] == '"')) { + text = syminsTrimQuotes(raw); // literal + } else { + text = syminsFeatAttr(f, std.mem.trim(u8, raw, " \t")) orelse return null; + } + color_idx = 7; + display_idx = 8; + } + if (text.len == 0) return null; + var color = std.mem.trim(u8, syminsArgAt(args, color_idx), " \t"); + if (color.len == 0) color = "CHBLK"; + const group = std.fmt.parseInt(i64, std.mem.trim(u8, syminsArgAt(args, display_idx), " \t"), 10) catch 0; + return instructions.Text{ .text = text, .color = color, .group = group }; +} + +/// Build an S-101 Portrayal from a NEWOBJ's SYMINS attribute, or null when there is +/// no usable SYMINS (caller then falls back to the default new-object symbology). +/// Geometry/anchoring/clipping is handled by processFeatureParsed exactly like a rule stream. +pub fn buildSyminsPortrayal(a: Allocator, f: s57.Feature) !?instructions.Portrayal { + const raw0 = f.attr(SYMINS_ATTR) orelse return null; + const raw = std.mem.trim(u8, raw0, " "); + if (raw.len == 0) return null; + + var points = std.ArrayList(instructions.Point).empty; + var texts = std.ArrayList(instructions.Text).empty; + var lines = std.ArrayList(instructions.Line).empty; + var patterns = std.ArrayList([]const u8).empty; + var fill_token: ?[]const u8 = null; + + for (try syminsSplitInstructions(a, raw)) |instr| { + const opp = syminsSplitOp(instr) orelse continue; + if (std.mem.eql(u8, opp.op, "SY")) { // SY(NAME[,rot]) + const args = try syminsSplitArgs(a, opp.params); + const name = std.mem.trim(u8, syminsArgAt(args, 0), " \t"); + if (name.len == 0) continue; + const rot: f64 = if (args.len > 1) (std.fmt.parseFloat(f64, std.mem.trim(u8, args[1], " \t")) catch 0) else 0; + try points.append(a, .{ .symbol = name, .rotation = rot, .offset_x = 0, .offset_y = 0 }); + } else if (std.mem.eql(u8, opp.op, "TX") or std.mem.eql(u8, opp.op, "TE")) { + if (try syminsText(a, f, opp.op, opp.params)) |t| try texts.append(a, t); + } else if (std.mem.eql(u8, opp.op, "LS")) { // LS(style,width,colour) + const args = try syminsSplitArgs(a, opp.params); + if (args.len < 3) continue; + var w = std.fmt.parseInt(i64, std.mem.trim(u8, args[1], " \t"), 10) catch 0; + if (w <= 0) w = 1; + const st = std.mem.trim(u8, args[0], " \t"); + const dashed = std.ascii.eqlIgnoreCase(st, "DASH") or std.ascii.eqlIgnoreCase(st, "DOTT"); + try lines.append(a, .{ + .style = if (dashed) "dash" else "solid", + .width = @floatFromInt(w), + .color = std.mem.trim(u8, args[2], " \t"), + }); + } else if (std.mem.eql(u8, opp.op, "LC")) { // LC(LINESTYLE) — approximated as dashed + const name = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); + if (name.len == 0) continue; + try lines.append(a, .{ .style = name, .width = 1, .color = "CHBLK" }); + } else if (std.mem.eql(u8, opp.op, "AC")) { // AC(COLOUR[,transp]) + const color = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); + if (color.len > 0) fill_token = color; + } else if (std.mem.eql(u8, opp.op, "AP")) { // AP(PATTERN) + const name = std.mem.trim(u8, syminsArgAt(try syminsSplitArgs(a, opp.params), 0), " \t"); + if (name.len > 0) try patterns.append(a, name); + } + } + if (points.items.len == 0 and texts.items.len == 0 and lines.items.len == 0 and + patterns.items.len == 0 and fill_token == null) return null; + return instructions.Portrayal{ + .fill_token = fill_token, + .patterns = patterns.items, + .lines = lines.items, + .points = points.items, + .texts = texts.items, + }; +} + +test "buildSyminsPortrayal parses SY/TX/LS/LC/AC/AP" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const attrs = [_]s57.Attr{.{ + .code = SYMINS_ATTR, + .value = "SY(BOYSPP01,45);TX('Hello',1,2,0,'15110',0,0,CHRED,28);" ++ + "LS(DASH,3,CHGRD);LS(SOLD,2,CHBLK);LC(NAVARE51);AC(DEPVS);AP(DIAMOND1)", + }}; + const f = s57.Feature{ .rcnm = 100, .rcid = 1, .prim = 3, .objl = 163, .attrs = &attrs }; + + const p = (try buildSyminsPortrayal(a, f)) orelse return error.NoPortrayal; + + try std.testing.expectEqual(@as(usize, 1), p.points.len); + try std.testing.expectEqualStrings("BOYSPP01", p.points[0].symbol); + try std.testing.expectEqual(@as(f64, 45), p.points[0].rotation); + + try std.testing.expectEqual(@as(usize, 1), p.texts.len); + try std.testing.expectEqualStrings("Hello", p.texts[0].text); + try std.testing.expectEqualStrings("CHRED", p.texts[0].color); + try std.testing.expectEqual(@as(i64, 28), p.texts[0].group); + + try std.testing.expectEqual(@as(usize, 3), p.lines.len); // 2x LS + 1x LC + try std.testing.expectEqualStrings("dash", p.lines[0].style); // DASH -> dashed + try std.testing.expectEqual(@as(f64, 3), p.lines[0].width); + try std.testing.expectEqualStrings("CHGRD", p.lines[0].color); + try std.testing.expectEqualStrings("solid", p.lines[1].style); // SOLD -> solid + try std.testing.expectEqualStrings("NAVARE51", p.lines[2].style); // LC name verbatim + + try std.testing.expectEqualStrings("DEPVS", p.fill_token.?); + try std.testing.expectEqual(@as(usize, 1), p.patterns.len); + try std.testing.expectEqualStrings("DIAMOND1", p.patterns[0]); + + // A blank / absent SYMINS yields no portrayal. + const f_empty = s57.Feature{ .rcnm = 100, .rcid = 2, .prim = 3, .objl = 163, .attrs = &[_]s57.Attr{.{ .code = SYMINS_ATTR, .value = " " }} }; + try std.testing.expect((try buildSyminsPortrayal(a, f_empty)) == null); + + // Instruction-splitting honours a ';' inside a quoted TX string. + const f_semi = s57.Feature{ .rcnm = 100, .rcid = 3, .prim = 1, .objl = 163, .attrs = &[_]s57.Attr{.{ .code = SYMINS_ATTR, .value = "TX('a;b',1,2,0,'15110',0,0,CHBLK,28)" }} }; + const ps = (try buildSyminsPortrayal(a, f_semi)) orelse return error.NoPortrayal; + try std.testing.expectEqual(@as(usize, 1), ps.texts.len); + try std.testing.expectEqualStrings("a;b", ps.texts[0].text); +} diff --git a/src/sprite/sprite.zig b/src/sprite/sprite.zig index 6d17f5b..77155b5 100644 --- a/src/sprite/sprite.zig +++ b/src/sprite/sprite.zig @@ -18,7 +18,9 @@ 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; } +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; diff --git a/src/assets/style.zig b/src/style/maplibre.zig similarity index 91% rename from src/assets/style.zig rename to src/style/maplibre.zig index 18122d3..0d2353d 100644 --- a/src/assets/style.zig +++ b/src/style/maplibre.zig @@ -1,9 +1,8 @@ -//! style.zig — MapLibre GL style.json generation for the chart bundle. Resolves -//! each S-52 colour token to hex for a palette and emits the fill / line / symbol -//! / text layer set. Ported from the chartplotter web frontend's s52-style.mjs / -//! chart-style.mjs (and the now-removed style/build_style.py, kept in git history; -//! it was verified layer-for-layer identical during the port). The sole style -//! generator now. Part of the `assets` module. +//! maplibre.zig — MapLibre GL style.json generation. Resolves each S-52 colour +//! token to hex for a palette and emits the fill / line / symbol / text layer +//! set. Ported from the chartplotter web frontend's s52-style.mjs / +//! chart-style.mjs, verified layer-for-layer identical during the port. The one +//! style.json generator; part of the `style` module. //! //! MapLibre expressions are written as Zig comptime tuples — `.{ "get", "drval1" }` //! serialises to `["get","drval1"]` — through std.json's Stringify write-stream, @@ -13,18 +12,18 @@ const std = @import("std"); const Stringify = std.json.Stringify; -const chartstyle = @import("chartstyle.zig"); +const mariner = @import("mariner.zig"); const FALLBACK = "#ff00ff"; const FONT = .{"Noto Sans Regular"}; // ---- MapLibre expressions, as comptime tuples ---------------------------- // SEABED01 depth shading, the danger-symbol swap, and the sounding bold/faint split -// are mariner-dependent and now resolve through the chartstyle builders (one style +// are mariner-dependent and now resolve through the mariner builders (one style // builder); only the mariner-INDEPENDENT layout exprs remain comptime tuples here. // The static (no-manifest) SCAMIN/oscl gate is now expressed as K / 2^zoom — -// the SAME display-denominator form the bucket path uses (styleJson computes +// the SAME display-denominator form the bucket path uses (json computes // K = M_PER_PX_Z0·cos(scamin_lat)/(pitch/1000) once per archive). The old hardcoded // DENOM_Z0 (0.28 mm OGC pixel, equator) gated ~0.4 z late at mid-latitude and used // the uncalibrated CSS pixel; K is latitude-corrected and calibrated. See @@ -51,7 +50,7 @@ pub fn scaminDisplayZoom(scamin: f64, lat: f64) f64 { /// The per-archive display-denominator constant K such that the on-screen 1:N /// denominator at Web-Mercator `zoom` is K / 2^zoom (== displayDenom). Baked into the /// static SCAMIN/oscl gate at the archive-center latitude; the SAME constant the -/// bucket path computes (styleJson). Replaces the old equator-only OGC DENOM_Z0. +/// bucket path computes (json). Replaces the old equator-only OGC DENOM_Z0. pub fn scaminGateK(lat: f64) f64 { return M_PER_PX_Z0 * @cos(lat * std.math.pi / 180.0) / (DEFAULT_PX_PITCH_MM / 1000.0); } @@ -157,7 +156,7 @@ const TEXT_OFFSET = .{ .{ "literal", .{ 0, 0 } }, }; -pub const StyleOpts = struct { +pub const Options = struct { scheme: []const u8, // "day" | "dusk" | "night" colortables_json: []const u8, source_tiles: ?[]const u8 = null, // tiles template; else pmtiles_url @@ -182,10 +181,10 @@ pub const StyleOpts = struct { // S-52 mariner display options. null = a TEMPLATE (no client display filters // baked in — the bundle + tile57_style_template path; the client gates live). // Non-null = the full style with the mariner's display filters + depth shading / - // sounding-split / danger-swap baked in (the C-ABI tile57_build_style path). - // Colour + layout always resolve through the chartstyle builders (default mariner - // when null), so there is ONE style builder — chartstyle.buildStyle is retired. - mariner: ?chartstyle.MarinerSettings = null, + // sounding-split / danger-swap baked in (the C-ABI tile57_style_build path). + // Colour + layout always resolve through the mariner builders (default mariner + // when null), so there is ONE style builder — the mariner builders is retired. + mariner: ?mariner.Settings = null, enabled_bands: ?[]const i32 = null, // mariner NOAA-band filter (null = all bands) now_unix: i64 = 0, // host wall-clock (epoch s) for the date filter's "today" // §2 ?ignoreScamin host debug toggle: when true, disable SCAMIN scale-gating @@ -213,8 +212,8 @@ pub const StyleOpts = struct { }; // Precomputed, mariner-aware style expressions shared by every layer of one -// styleJson call — the single style builder. Colours / depth shading / icon images -// resolve ONCE through the chartstyle builders (so chartstyle.buildStyle's patch pass +// json call — the single style builder. Colours / depth shading / icon images +// resolve ONCE through the mariner builders // is retired); `common`/`text_group` are the S-52 display filters, empty/null in // template mode (opts.mariner == null) so a template renders without baked gating. const SCtx = struct { @@ -247,7 +246,7 @@ const DenomGate = union(enum) { zoom_k: f64, }; -// The overscale (oscl) clause — S-52 §10.1.10 (specs/overscale.md). The +// The overscale (oscl) clause — S-52 §10.1.10. The // filter-gate form is EXACTLY // [">", ["coalesce", ["get","oscl"], 0], DENOM] // ("the display is FINER than the cell's quantized compilation scale"), or its @@ -320,12 +319,12 @@ const Bucket = struct { filter_gate: bool = false, // scamin-layers.md: the live client-driven SCAMIN clause (?scaminexact) cur_denom: f64 = 0, // filter_gate: the current-display-scale denominator literal (client-overwritten) suffix: []const u8 = "", // id suffix: "#oscl" (overscaled fill pass) / "" (plain) - // Overscale (oscl) clause role (S-52 §10.1.10, specs/overscale.md) — see OverscaleRole. + // Overscale (oscl) clause role (S-52 §10.1.10) — see OverscaleRole. oscl: OverscaleRole = .none, }; // Overscale (oscl) clause role a fill/pattern layer plays in the AP(OVERSC01) -// occlusion sandwich (S-52 §10.1.10, specs/overscale.md): +// occlusion sandwich (S-52 §10.1.10): // .none — no oscl clause (every layer outside the sandwich). // .overscaled — [">", coalesce(oscl,0), DENOM]: this cell's data is displayed FINER // than its compilation scale (the fills under the hatch + the hatch itself). @@ -446,7 +445,7 @@ fn pointLayout(js: *Stringify, alignment: []const u8, icon: std.json.Value, scal // first = underneath). draw_prio is the SOLE axis; the danger-over-sounding // deviation is a sort VALUE (effective 19), not a class tier. z-order "auto" makes // the sort-key take effect. Mirrors fill-sort-key on the fill layers. NOTE: this - // orders WITHIN one layer only; LIGHTS get their own top layer set (see styleJson). + // orders WITHIN one layer only; LIGHTS get their own top layer set (see json). try js.objectField("symbol-sort-key"); try js.write(SYMBOL_SORT); try js.objectField("symbol-z-order"); @@ -669,11 +668,11 @@ fn patternLayer(js: *Stringify, s: *const SCtx, sl: []const u8, bkt: Bucket) !vo try js.endObject(); } -// The AP(OVERSC01) overscale-indication layer (S-52 §10.1.10, specs/overscale.md): +// The AP(OVERSC01) overscale-indication layer (S-52 §10.1.10): // every contributing cell's M_COVR coverage polygon (baked into `area_patterns` // as pattern OVERSC01, tagged `oscl`), shown only while the display is FINER than // the cell's quantized compilation scale (the oscl clause). Sandwiched between -// the overscaled and at-scale fill passes (styleJson §2), so a finer cell's +// the overscaled and at-scale fill passes (json §2), so a finer cell's // opaque fills occlude a coarser cell's hatch — the hatch survives only on // coarse-only patches. The showOverscale mariner toggle drives layout.visibility // alone (layer set unchanged -> a one-op style diff). @@ -794,7 +793,7 @@ fn dangerSoundingsLayer(js: *Stringify, s: *const SCtx, bkt: Bucket) !void { } /// Emit a MapLibre style.json for `opts.scheme`. Returns allocator-owned bytes. -pub fn styleJson(alloc: std.mem.Allocator, opts: StyleOpts) ![]u8 { +pub fn json(alloc: std.mem.Allocator, opts: Options) ![]u8 { var parsed = std.json.parseFromSlice(std.json.Value, alloc, opts.colortables_json, .{}) catch return error.BadColortables; defer parsed.deinit(); @@ -835,28 +834,28 @@ pub fn styleJson(alloc: std.mem.Allocator, opts: StyleOpts) ![]u8 { const scamin_buckets: []const Bucket = &.{gate}; // The single style builder: resolve every mariner-aware colour / icon / display - // filter ONCE through the chartstyle builders (retiring chartstyle.buildStyle's + // filter ONCE through the mariner builders // template-patch pass). scheme always comes from opts.scheme (the bundle emits one // style per scheme); a null opts.mariner is a TEMPLATE — default mariner for the // colour/layout exprs, but NO display filters baked in (the client gates live). - const scheme_e: chartstyle.Scheme = if (std.mem.eql(u8, opts.scheme, "night")) + const scheme_e: mariner.Scheme = if (std.mem.eql(u8, opts.scheme, "night")) .night else if (std.mem.eql(u8, opts.scheme, "dusk")) .dusk else .day; - var m: chartstyle.MarinerSettings = opts.mariner orelse .{}; + var m: mariner.Settings = opts.mariner orelse .{}; m.scheme = scheme_e; const filters_on = opts.mariner != null; - const b = chartstyle.B{ .a = ba }; + const b = mariner.B{ .a = ba }; const s = SCtx{ - .fill_color = try chartstyle.areasFillColor(b, &palette, &m), - .line_color = try chartstyle.lineColor(b, &palette), - .text_color = try chartstyle.textColor(b, m.scheme, &palette), - .halo = chartstyle.textHaloColor(b, m.scheme), - .contour_color = try chartstyle.contourLabelColor(b, m.scheme, &palette), - .sound_img = try chartstyle.soundingsIconImage(b, &m), - .point_img = try chartstyle.pointSymbolImage(b, &m), - .contour_field = try chartstyle.contourLabelField(b, &m), - .common = if (filters_on) try chartstyle.commonChartFilters(ba, &m, opts.enabled_bands, opts.now_unix) else &.{}, - .text_group = if (filters_on) try chartstyle.textGroupFilter(b, &m) else null, + .fill_color = try mariner.areasFillColor(b, &palette, &m), + .line_color = try mariner.lineColor(b, &palette), + .text_color = try mariner.textColor(b, m.scheme, &palette), + .halo = mariner.textHaloColor(b, m.scheme), + .contour_color = try mariner.contourLabelColor(b, m.scheme, &palette), + .sound_img = try mariner.soundingsIconImage(b, &m), + .point_img = try mariner.pointSymbolImage(b, &m), + .contour_field = try mariner.contourLabelField(b, &m), + .common = if (filters_on) try mariner.commonChartFilters(ba, &m, opts.enabled_bands, opts.now_unix) else &.{}, + .text_group = if (filters_on) try mariner.textGroupFilter(b, &m) else null, .size_scale = opts.size_scale, // Overscale gate mode follows the SCAMIN gating mode: the filter-gate // literal when the live client drives it (same injected DENOM), else the @@ -936,7 +935,7 @@ pub fn styleJson(alloc: std.mem.Allocator, opts: StyleOpts) ![]u8 { // the hatch) each bucket's fill splits around the AP(OVERSC01) overscale layer: // overscaled cells' fills (#oscl) UNDER the hatch, at-scale fills ABOVE it — so // finer opaque DEPARE/LNDARE occlude a coarser cell's hatch and it survives only - // on coarse-only patches (S-52 §10.1.10, specs/overscale.md). ignore_scamin (gate + // on coarse-only patches (S-52 §10.1.10). ignore_scamin (gate // off) or no sprite keeps a single plain fill layer per bucket. if (sprite_on) { for (scamin_buckets) |bkt| try fillLayer(js, &s, "areas", try bucketWithOverscale(ba, bkt, .overscaled)); @@ -1010,16 +1009,16 @@ pub fn styleJson(alloc: std.mem.Allocator, opts: StyleOpts) ![]u8 { } /// Build a full MapLibre style from a base template + mariner settings — the single -/// builder behind the C-ABI / WASM / parity callers (replaces chartstyle.buildStyle's +/// builder behind the C-ABI / WASM / parity callers (replaces the mariner builders's /// template-patch pass). The passed template carries ONLY the host's source config /// (sprite / glyphs / chart tiles+zoom); this lifts that out and regenerates every -/// layer via styleJson with the mariner baked in. Signature mirrors the retired +/// layer via json with the mariner baked in. Signature mirrors the retired /// buildStyle so callers are a one-line change. A bad template or unusable colortables /// returns the template bytes unchanged (alloc-owned dup), as buildStyle did. pub fn buildFromTemplate( alloc: std.mem.Allocator, template_json: []const u8, - m: *const chartstyle.MarinerSettings, + m: *const mariner.Settings, colortables_json: []const u8, enabled_bands: ?[]const i32, now_unix: i64, @@ -1032,13 +1031,13 @@ pub fn buildFromTemplate( /// Same as buildFromTemplate but threading a SCAMIN manifest (the distinct /// denominators present + a representative latitude), so the RUNTIME style gets the /// SAME per-value native-minzoom bucket layers the offline bundle does (host -/// host-canonical-backend.md §"Still needed" #1 — the `tile57_build_style` runtime +/// host-canonical-backend.md §"Still needed" #1 — the `tile57_style_build` runtime /// path otherwise leaves every `_scamin` layer ungated). Empty `scamin` == the /// plain buildFromTemplate behaviour. pub fn buildFromTemplateScamin( alloc: std.mem.Allocator, template_json: []const u8, - m: *const chartstyle.MarinerSettings, + m: *const mariner.Settings, colortables_json: []const u8, enabled_bands: ?[]const i32, now_unix: i64, @@ -1048,7 +1047,7 @@ pub fn buildFromTemplateScamin( var parsed = std.json.parseFromSlice(std.json.Value, alloc, template_json, .{}) catch return alloc.dupe(u8, template_json); defer parsed.deinit(); - var opts = StyleOpts{ + var opts = Options{ .scheme = switch (m.scheme) { .dusk => "dusk", .night => "night", @@ -1083,7 +1082,7 @@ pub fn buildFromTemplateScamin( if (t == .array and t.array.items.len > 0 and t.array.items[0] == .string) opts.source_tiles = t.array.items[0].string; } - if (c.get("encoding")) |e| { // MLT hint rides the rebuild (tile57_build_style / style_diff) + if (c.get("encoding")) |e| { // MLT hint rides the rebuild (tile57_style_build / style_diff) if (e == .string) opts.encoding = e.string; } if (c.get("minzoom")) |z| { @@ -1096,12 +1095,12 @@ pub fn buildFromTemplateScamin( }; } } - return styleJson(alloc, opts) catch alloc.dupe(u8, template_json); + return json(alloc, opts) catch alloc.dupe(u8, template_json); } -// ---- style diff (style-diff.md) -------------------------------------------- +// ---- style diff -------------------------------------------- // Compute the minimal MapLibre style-mutation ops that turn one built style into -// another. The engine knows both styles come from styleJson (same layer set + +// another. The engine knows both styles come from json (same layer set + // deterministic key order), so a structural layer-by-layer compare yields exactly // the ops MapLibre's setStyle(diff:true) would — but scoped to the chart layers and // returned as data the host applies with setFilter / setPaintProperty / @@ -1127,7 +1126,7 @@ fn layerId(v: std.json.Value) ?[]const u8 { } /// Structural deep-equality for two parsed JSON values. std.json.Value has no -/// built-in equal; both operands here come from the same styleJson generator, so +/// built-in equal; both operands here come from the same json generator, so /// corresponding keys share a representation (integer stays integer, object key /// order matches) and a recursive compare is exact. fn jsonEql(a: std.json.Value, b: std.json.Value) bool { @@ -1211,12 +1210,12 @@ fn diffSubObject(js: *Stringify, op: []const u8, id: []const u8, old_v: ?std.jso } /// Compute the minimal MapLibre style-mutation ops (a JSON array) to turn the -/// serialized style `old_json` into `new_json`. Both must be styleJson output (same +/// serialized style `old_json` into `new_json`. Both must be json output (same /// template/colortables/bands/scamin inputs, differing only in mariner). Returns /// allocator-owned bytes: `"[]"` when nothing differs; one op per differing /// `filter` / `paint.*` / `layout.*` key; `[{"op":"rebuild"}]` when the two styles /// carry a different SET of layer ids (the host then falls back to a full setStyle). -pub fn styleDiff(alloc: std.mem.Allocator, old_json: []const u8, new_json: []const u8) ![]u8 { +pub fn diff(alloc: std.mem.Allocator, old_json: []const u8, new_json: []const u8) ![]u8 { var old_parsed = std.json.parseFromSlice(std.json.Value, alloc, old_json, .{}) catch return alloc.dupe(u8, rebuild_ops); defer old_parsed.deinit(); @@ -1263,11 +1262,11 @@ pub fn styleDiff(alloc: std.mem.Allocator, old_json: []const u8, new_json: []con return aw.toOwnedSlice(); } -test "styleJson: valid JSON, expected layers, palette-resolved colour" { +test "json: valid JSON, expected layers, palette-resolved colour" { const ct = \\{"day":{"DEPDW":"#c9edff","CHGRD":"#4c5b63","CHBLK":"#000000"},"dusk":{},"night":{"DEPDW":"#0a141e"}} ; - const out = try styleJson(std.testing.allocator, .{ + const out = try json(std.testing.allocator, .{ .scheme = "day", .colortables_json = ct, .source_tiles = "tile57://{z}/{x}/{y}", @@ -1287,13 +1286,13 @@ test "styleJson: valid JSON, expected layers, palette-resolved colour" { try std.testing.expect(std.mem.indexOf(u8, out, "#c9edff") != null); } -test "styleJson: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { +test "json: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 30000, 90000 }; - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1303,7 +1302,7 @@ test "styleJson: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { // Manifest present, gating ON -> the merged zoom-gate rides every layer (per-value // #sm buckets are retired — a manifest never buckets now). - const gated = try styleJson(a, base); + const gated = try json(a, base); defer a.free(gated); try std.testing.expect(std.mem.indexOf(u8, gated, "[\">=\",[\"coalesce\",[\"get\",\"scamin\"],1000000000000],[\"/\",") != null); try std.testing.expect(std.mem.indexOf(u8, gated, "#sm") == null); @@ -1311,7 +1310,7 @@ test "styleJson: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { // Manifest present, ignore_scamin -> no buckets at all. var ign = base; ign.ignore_scamin = true; - const out_ign = try styleJson(a, ign); + const out_ign = try json(a, ign); defer a.free(out_ign); try std.testing.expect(std.mem.indexOf(u8, out_ign, "#sm") == null); @@ -1319,7 +1318,7 @@ test "styleJson: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { // scamin>=D(zoom) fallback shape, no per-value buckets). var nomanifest = base; nomanifest.scamin = &.{}; - const out_fb = try styleJson(a, nomanifest); + const out_fb = try json(a, nomanifest); defer a.free(out_fb); try std.testing.expect(std.mem.indexOf(u8, out_fb, "[\">=\",[\"coalesce\",[\"get\",\"scamin\"],1000000000000],[\"/\",") != null); try std.testing.expect(std.mem.indexOf(u8, out_fb, "#sm") == null); @@ -1328,7 +1327,7 @@ test "styleJson: ignore_scamin drops SCAMIN gating (no buckets, no zoom-gate)" { // No manifest + ignore_scamin -> even the static gate is gone. var nm_ign = nomanifest; nm_ign.ignore_scamin = true; - const out_nm_ign = try styleJson(a, nm_ign); + const out_nm_ign = try json(a, nm_ign); defer a.free(out_nm_ign); try std.testing.expect(std.mem.indexOf(u8, out_nm_ign, "[\">=\",[\"coalesce\",[\"get\",\"scamin\"],1000000000000],[\"/\",") == null); try std.testing.expect(std.mem.indexOf(u8, out_nm_ign, "#sm") == null); @@ -1339,12 +1338,12 @@ fn layerIndexById(layers: []std.json.Value, id: []const u8) ?usize { return null; } -test "styleJson: point z-order = draw_prio alone (threshold partition + danger sort-value)" { +test "json: point z-order = draw_prio alone (threshold partition + danger sort-value)" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; - const out = try styleJson(a, .{ + const out = try json(a, .{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1379,12 +1378,12 @@ test "styleJson: point z-order = draw_prio alone (threshold partition + danger s try std.testing.expect(i_over < i_lt); } -test "styleJson: size_scale wraps icon/line/text sizes in a multiplier" { +test "json: size_scale wraps icon/line/text sizes in a multiplier" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1392,7 +1391,7 @@ test "styleJson: size_scale wraps icon/line/text sizes in a multiplier" { }; // Default scale 1.0: sizes written verbatim, no multiplier wrapper. - const def = try styleJson(a, base); + const def = try json(a, base); defer a.free(def); try std.testing.expect(std.mem.indexOf(u8, def, "\"line-width\":[\"coalesce\",[\"get\",\"width_px\"],1]") != null); try std.testing.expect(std.mem.indexOf(u8, def, "\"line-width\":[\"*\"") == null); @@ -1400,7 +1399,7 @@ test "styleJson: size_scale wraps icon/line/text sizes in a multiplier" { // Scaled: icon-size / line-width / text-size each wrap in ["*", scale, expr]. var scaled = base; scaled.size_scale = 2.0; - const sc = try styleJson(a, scaled); + const sc = try json(a, scaled); defer a.free(sc); try std.testing.expect(std.mem.indexOf(u8, sc, "\"line-width\":[\"*\"") != null); try std.testing.expect(std.mem.indexOf(u8, sc, "\"icon-size\":[\"*\"") != null); @@ -1410,7 +1409,7 @@ test "styleJson: size_scale wraps icon/line/text sizes in a multiplier" { } // ---- single-builder (buildFromTemplate) tests — ported from the retired -// chartstyle.buildStyle tests, now exercising the one styleJson path. ------- +// the mariner builders tests, now exercising the one json path. ------- const cs_template = \\{"version":8,"sources":{"chart":{"type":"vector","url":"pmtiles://x"}},"sprite":"x","glyphs":"x","layers":[]} ; @@ -1420,7 +1419,7 @@ const cs_ct = test "buildFromTemplate: defaults bake SEABED fill + category/M_QUAL filter (single-pass)" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{}; + const m = mariner.Settings{}; const out = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(out); try std.testing.expect(std.mem.indexOf(u8, out, "DEPMD") != null); // SEABED01 band @@ -1432,7 +1431,7 @@ test "buildFromTemplate: defaults bake SEABED fill + category/M_QUAL filter (sin test "buildFromTemplate: night scheme -> neutral ink + dark halo" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{ .scheme = .night }; + const m = mariner.Settings{ .scheme = .night }; const out = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(out); try std.testing.expect(std.mem.indexOf(u8, out, "#aab7bf") != null); @@ -1441,7 +1440,7 @@ test "buildFromTemplate: night scheme -> neutral ink + dark halo" { test "buildFromTemplate: feet depth unit -> contour label uses M_TO_FT" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{ .depth_unit = .feet }; + const m = mariner.Settings{ .depth_unit = .feet }; const out = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(out); try std.testing.expect(std.mem.indexOf(u8, out, "3.280839895") != null); @@ -1462,7 +1461,7 @@ test "buildFromTemplate: feet picks the sounding feet glyph variant; metres does test "buildFromTemplate: enabled bands add a band filter" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{}; + const m = mariner.Settings{}; const bands = [_]i32{ 2, 3 }; const out = try buildFromTemplate(a, cs_template, &m, cs_ct, &bands, 1700000000); defer a.free(out); @@ -1471,18 +1470,18 @@ test "buildFromTemplate: enabled bands add a band filter" { test "buildFromTemplate: date resolution (pinned + today + off)" { const a = std.testing.allocator; - const m1 = chartstyle.MarinerSettings{ .date_view = "20240115" }; + const m1 = mariner.Settings{ .date_view = "20240115" }; const o1 = try buildFromTemplate(a, cs_template, &m1, cs_ct, null, 1700000000); defer a.free(o1); try std.testing.expect(std.mem.indexOf(u8, o1, "20240115") != null); try std.testing.expect(std.mem.indexOf(u8, o1, "0115") != null); - const m2 = chartstyle.MarinerSettings{}; + const m2 = mariner.Settings{}; const o2 = try buildFromTemplate(a, cs_template, &m2, cs_ct, null, 1700000000); defer a.free(o2); try std.testing.expect(std.mem.indexOf(u8, o2, "20231114") != null); - const m3 = chartstyle.MarinerSettings{ .date_dependent = false }; + const m3 = mariner.Settings{ .date_dependent = false }; const o3 = try buildFromTemplate(a, cs_template, &m3, cs_ct, null, 1700000000); defer a.free(o3); try std.testing.expect(std.mem.indexOf(u8, o3, "date_recurring") == null); @@ -1493,7 +1492,7 @@ test "buildFromTemplate: viewing-group deny-list filter gates by vg" { // A non-empty off-set hides the listed groups -> the style references the `vg` // property and the off ids, negated (deny-list). const off = [_]i32{ 26070, 27070 }; - const m = chartstyle.MarinerSettings{ .viewing_groups_off = &off }; + const m = mariner.Settings{ .viewing_groups_off = &off }; const out = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(out); try std.testing.expect(std.mem.indexOf(u8, out, "\"vg\"") != null); @@ -1501,13 +1500,13 @@ test "buildFromTemplate: viewing-group deny-list filter gates by vg" { // The filter is a deny-list: ["!",["in",...]] so the off groups are EXCLUDED. try std.testing.expect(std.mem.indexOf(u8, out, "\"in\"") != null); // null off-set -> no vg filter at all. - const m2 = chartstyle.MarinerSettings{}; + const m2 = mariner.Settings{}; const o2 = try buildFromTemplate(a, cs_template, &m2, cs_ct, null, 1700000000); defer a.free(o2); try std.testing.expect(std.mem.indexOf(u8, o2, "\"vg\"") == null); // empty off-set -> also no filter (show all). const empty = [_]i32{}; - const m3 = chartstyle.MarinerSettings{ .viewing_groups_off = &empty }; + const m3 = mariner.Settings{ .viewing_groups_off = &empty }; const o3 = try buildFromTemplate(a, cs_template, &m3, cs_ct, null, 1700000000); defer a.free(o3); try std.testing.expect(std.mem.indexOf(u8, o3, "\"vg\"") == null); @@ -1515,7 +1514,7 @@ test "buildFromTemplate: viewing-group deny-list filter gates by vg" { test "buildFromTemplateScamin: a manifest no longer buckets — the merged zoom-gate rides every layer" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{}; + const m = mariner.Settings{}; // No manifest -> the static K/2^zoom SCAMIN zoom-gate, no #sm buckets. const plain = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(plain); @@ -1533,17 +1532,16 @@ test "buildFromTemplateScamin: a manifest no longer buckets — the merged zoom- // ---- smax removal ----------------------------------------------------------- -test "styleJson: no smax clause in any gating mode (band-handoff gate retired)" { - // The coverage-clipped composite owns cross-band occlusion geometrically - // (specs/cross-band-composition-redesign.md §3): no layer carries a - // band-handoff smax clause any more, in any gating mode — and the overscale - // gate survives ?ignoreScamin (decoupled per spec §5). +test "json: no smax clause in any gating mode (band-handoff gate retired)" { + // The coverage-clipped composite owns cross-band occlusion geometrically, so + // no layer carries a band-handoff smax clause in any gating mode, and the + // overscale gate survives ?ignoreScamin (the two gates are decoupled). const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 45000, 90000 }; - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1555,40 +1553,40 @@ test "styleJson: no smax clause in any gating mode (band-handoff gate retired)" var gated_opts = base; gated_opts.scamin_filter_gate = true; gated_opts.scamin_cur_denom = 50000; - const gated = try styleJson(a, gated_opts); + const gated = try json(a, gated_opts); defer a.free(gated); try std.testing.expect(std.mem.indexOf(u8, gated, "smax") == null); // The scamin clause still gates (same injected DENOM literal). try std.testing.expect(std.mem.indexOf(u8, gated, "[\">=\",[\"coalesce\",[\"get\",\"scamin\"],1000000000000],50000]") != null); - const bucketed = try styleJson(a, base); + const bucketed = try json(a, base); defer a.free(bucketed); try std.testing.expect(std.mem.indexOf(u8, bucketed, "smax") == null); var nm = base; nm.scamin = &.{}; - const fb = try styleJson(a, nm); + const fb = try json(a, nm); defer a.free(fb); try std.testing.expect(std.mem.indexOf(u8, fb, "smax") == null); var ign = base; ign.ignore_scamin = true; - const out_ign = try styleJson(a, ign); + const out_ign = try json(a, ign); defer a.free(out_ign); try std.testing.expect(std.mem.indexOf(u8, out_ign, "smax") == null); // ignoreScamin no longer kills the overscale indication (spec §5). try std.testing.expect(std.mem.indexOf(u8, out_ign, "oscl") != null); } -// ---- overscale (oscl) gate tests (specs/overscale.md) ----------------------- +// ---- overscale (oscl) gate tests ------------------------------------------- -test "styleJson: the overscale oscl clause has the EXACT client-matched shape" { +test "json: the overscale oscl clause has the EXACT client-matched shape" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 45000, 90000 }; - const gated = try styleJson(a, .{ + const gated = try json(a, .{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1612,7 +1610,7 @@ test "styleJson: the overscale oscl clause has the EXACT client-matched shape" { // The boot/diff PLACEHOLDER (cur_denom 0) maps to 1e12 — hide the hatch and // keep every fill in the at-scale pass (today's rendering) until the client // injects the live denominator (the placeholder lesson, cb91c4d). - const boot = try styleJson(a, .{ + const boot = try json(a, .{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1649,13 +1647,13 @@ test "styleJson: the overscale oscl clause has the EXACT client-matched shape" { try std.testing.expectEqualStrings("visible", hatch.get("layout").?.object.get("visibility").?.string); } -test "styleJson: showOverscale=false hides the hatch layer; ignoreScamin keeps it (decoupled)" { +test "json: showOverscale=false hides the hatch layer; ignoreScamin keeps it (decoupled)" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 45000, 90000 }; - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1667,14 +1665,14 @@ test "styleJson: showOverscale=false hides the hatch layer; ignoreScamin keeps i // so a style diff is a single setLayoutProperty op). var off = base; off.mariner = .{ .show_overscale = false }; - const hidden = try styleJson(a, off); + const hidden = try json(a, off); defer a.free(hidden); try std.testing.expect(std.mem.indexOf(u8, hidden, "\"id\":\"overscale\",") != null); try std.testing.expect(std.mem.indexOf(u8, hidden, "{\"visibility\":\"none\"}") != null); try std.testing.expect(std.mem.indexOf(u8, hidden, "\"oscl\"") != null); // Bucket mode derives the oscl DENOM from zoom (same K as the scamin clause). - const bucketed = try styleJson(a, base); + const bucketed = try json(a, base); defer a.free(bucketed); try std.testing.expect(std.mem.indexOf(u8, bucketed, "[\">\",[\"coalesce\",[\"get\",\"oscl\"],0],[\"/\",") != null); @@ -1682,7 +1680,7 @@ test "styleJson: showOverscale=false hides the hatch layer; ignoreScamin keeps i // (spec §5) — the oscl clauses + hatch layer survive the debug toggle. var ign = base; ign.ignore_scamin = true; - const out_ign = try styleJson(a, ign); + const out_ign = try json(a, ign); defer a.free(out_ign); try std.testing.expect(std.mem.indexOf(u8, out_ign, "oscl") != null); try std.testing.expect(std.mem.indexOf(u8, out_ign, "\"id\":\"overscale\",") != null); @@ -1690,15 +1688,15 @@ test "styleJson: showOverscale=false hides the hatch layer; ignoreScamin keeps i // No sprite -> nothing can draw the hatch: single plain fill layer, no sandwich. var nospr = base; nospr.sprite = null; - const out_ns = try styleJson(a, nospr); + const out_ns = try json(a, nospr); defer a.free(out_ns); try std.testing.expect(std.mem.indexOf(u8, out_ns, "oscl") == null); try std.testing.expect(std.mem.indexOf(u8, out_ns, "\"id\":\"overscale\",") == null); } -// ---- styleDiff tests (style-diff.md §5) ------------------------------------ +// ---- diff tests ------------------------------------ -test "styleDiff: filter + paint + layout changes emit precise, scoped ops" { +test "diff: filter + paint + layout changes emit precise, scoped ops" { const a = std.testing.allocator; const old_j = \\{"layers":[ @@ -1712,7 +1710,7 @@ test "styleDiff: filter + paint + layout changes emit precise, scoped ops" { \\{"id":"text","type":"symbol","paint":{"text-color":"#000"}} \\]} ; - const ops = try styleDiff(a, old_j, new_j); + const ops = try diff(a, old_j, new_j); defer a.free(ops); // areas: filter, paint fill-color, layout visibility all changed -> one op each. try std.testing.expect(std.mem.indexOf(u8, ops, "{\"op\":\"setFilter\",\"layer\":\"areas\",\"value\":[\"==\",\"x\",2]}") != null); @@ -1722,11 +1720,11 @@ test "styleDiff: filter + paint + layout changes emit precise, scoped ops" { try std.testing.expect(std.mem.indexOf(u8, ops, "\"layer\":\"text\"") == null); } -test "styleDiff: a removed paint key emits value:null" { +test "diff: a removed paint key emits value:null" { const a = std.testing.allocator; const old_j = "{\"layers\":[{\"id\":\"l\",\"paint\":{\"fill-color\":\"#111\",\"fill-opacity\":0.5}}]}"; const new_j = "{\"layers\":[{\"id\":\"l\",\"paint\":{\"fill-color\":\"#111\"}}]}"; - const ops = try styleDiff(a, old_j, new_j); + const ops = try diff(a, old_j, new_j); defer a.free(ops); try std.testing.expectEqualStrings( "[{\"op\":\"setPaintProperty\",\"layer\":\"l\",\"property\":\"fill-opacity\",\"value\":null}]", @@ -1734,39 +1732,39 @@ test "styleDiff: a removed paint key emits value:null" { ); } -test "styleDiff: a differing layer-id set -> rebuild" { +test "diff: a differing layer-id set -> rebuild" { const a = std.testing.allocator; // id renamed - const r1 = try styleDiff(a, "{\"layers\":[{\"id\":\"x\"}]}", "{\"layers\":[{\"id\":\"y\"}]}"); + const r1 = try diff(a, "{\"layers\":[{\"id\":\"x\"}]}", "{\"layers\":[{\"id\":\"y\"}]}"); defer a.free(r1); try std.testing.expectEqualStrings(rebuild_ops, r1); // count differs - const r2 = try styleDiff(a, "{\"layers\":[{\"id\":\"x\"}]}", "{\"layers\":[{\"id\":\"x\"},{\"id\":\"z\"}]}"); + const r2 = try diff(a, "{\"layers\":[{\"id\":\"x\"}]}", "{\"layers\":[{\"id\":\"x\"},{\"id\":\"z\"}]}"); defer a.free(r2); try std.testing.expectEqualStrings(rebuild_ops, r2); } -test "styleDiff: same mariner -> [] (no ops)" { +test "diff: same mariner -> [] (no ops)" { const a = std.testing.allocator; - const m = chartstyle.MarinerSettings{}; + const m = mariner.Settings{}; const s1 = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(s1); const s2 = try buildFromTemplate(a, cs_template, &m, cs_ct, null, 1700000000); defer a.free(s2); - const ops = try styleDiff(a, s1, s2); + const ops = try diff(a, s1, s2); defer a.free(ops); try std.testing.expectEqualStrings("[]", ops); } -test "styleDiff: display_other flip emits only setFilter ops" { +test "diff: display_other flip emits only setFilter ops" { const a = std.testing.allocator; - const base = chartstyle.MarinerSettings{}; - const other = chartstyle.MarinerSettings{ .display_other = true }; + const base = mariner.Settings{}; + const other = mariner.Settings{ .display_other = true }; const s1 = try buildFromTemplate(a, cs_template, &base, cs_ct, null, 1700000000); defer a.free(s1); const s2 = try buildFromTemplate(a, cs_template, &other, cs_ct, null, 1700000000); defer a.free(s2); - const ops = try styleDiff(a, s1, s2); + const ops = try diff(a, s1, s2); defer a.free(ops); try std.testing.expect(std.mem.indexOf(u8, ops, "setFilter") != null); try std.testing.expect(std.mem.indexOf(u8, ops, "setPaintProperty") == null); @@ -1774,15 +1772,15 @@ test "styleDiff: display_other flip emits only setFilter ops" { try std.testing.expect(!std.mem.eql(u8, ops, "[]")); } -test "styleDiff: day vs night emits setPaintProperty colour ops, no filter change" { +test "diff: day vs night emits setPaintProperty colour ops, no filter change" { const a = std.testing.allocator; - const day = chartstyle.MarinerSettings{ .scheme = .day }; - const night = chartstyle.MarinerSettings{ .scheme = .night }; + const day = mariner.Settings{ .scheme = .day }; + const night = mariner.Settings{ .scheme = .night }; const s1 = try buildFromTemplate(a, cs_template, &day, cs_ct, null, 1700000000); defer a.free(s1); const s2 = try buildFromTemplate(a, cs_template, &night, cs_ct, null, 1700000000); defer a.free(s2); - const ops = try styleDiff(a, s1, s2); + const ops = try diff(a, s1, s2); defer a.free(ops); try std.testing.expect(std.mem.indexOf(u8, ops, "setPaintProperty") != null); try std.testing.expect(std.mem.indexOf(u8, ops, "setFilter") == null); @@ -1796,13 +1794,13 @@ fn layerCount(a: std.mem.Allocator, style: []const u8) !usize { return p.value.object.get("layers").?.array.items.len; } -test "styleJson: both merged modes (zoom-gate default, filter-gate exact) give one layer per render-type — no per-value buckets" { +test "json: both merged modes (zoom-gate default, filter-gate exact) give one layer per render-type — no per-value buckets" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 3000, 8000, 12000, 22000, 45000, 90000, 180000 }; // 7 denominators - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1813,7 +1811,7 @@ test "styleJson: both merged modes (zoom-gate default, filter-gate exact) give o // Default (a manifest present): the merged zoom-gate — ONE self-gating layer per // family, NO per-value #sm buckets, NO native minzoom. - const merged = try styleJson(a, base); + const merged = try json(a, base); defer a.free(merged); try std.testing.expect(std.mem.indexOf(u8, merged, "#sm") == null); try std.testing.expect(std.mem.indexOf(u8, merged, "minzoom") == null); @@ -1823,7 +1821,7 @@ test "styleJson: both merged modes (zoom-gate default, filter-gate exact) give o // set, with the client-driven curDenom clause instead of the zoom expression. var fg = base; fg.scamin_filter_gate = true; - const gated = try styleJson(a, fg); + const gated = try json(a, fg); defer a.free(gated); try std.testing.expect(std.mem.indexOf(u8, gated, "#sm") == null); // no per-value buckets try std.testing.expect(std.mem.indexOf(u8, gated, "minzoom") == null); // no native minzoom gating @@ -1835,13 +1833,13 @@ test "styleJson: both merged modes (zoom-gate default, filter-gate exact) give o try std.testing.expectEqual(try layerCount(a, merged), try layerCount(a, gated)); } -test "styleJson: scamin_filter_gate honors the cur_denom literal + ignore_scamin drops the clause" { +test "json: scamin_filter_gate honors the cur_denom literal + ignore_scamin drops the clause" { const a = std.testing.allocator; const ct = \\{"day":{"DEPDW":"#c9edff"},"dusk":{},"night":{}} ; const sm = [_]u32{ 45000, 90000 }; - const base = StyleOpts{ + const base = Options{ .scheme = "day", .colortables_json = ct, .sprite = "sprite", @@ -1851,7 +1849,7 @@ test "styleJson: scamin_filter_gate honors the cur_denom literal + ignore_scamin .scamin_filter_gate = true, .scamin_cur_denom = 50000, }; - const gated = try styleJson(a, base); + const gated = try json(a, base); defer a.free(gated); // The baked default denominator appears in the clause. try std.testing.expect(std.mem.indexOf(u8, gated, "50000") != null); @@ -1859,7 +1857,7 @@ test "styleJson: scamin_filter_gate honors the cur_denom literal + ignore_scamin // ignore_scamin overrides filter_gate: single plain layer, no SCAMIN clause at all. var ign = base; ign.ignore_scamin = true; - const out_ign = try styleJson(a, ign); + const out_ign = try json(a, ign); defer a.free(out_ign); try std.testing.expect(std.mem.indexOf(u8, out_ign, "1000000000000") == null); try std.testing.expect(std.mem.indexOf(u8, out_ign, "#sm") == null); diff --git a/src/assets/chartstyle.zig b/src/style/mariner.zig similarity index 88% rename from src/assets/chartstyle.zig rename to src/style/mariner.zig index 3a452fa..42e0132 100644 --- a/src/assets/chartstyle.zig +++ b/src/style/mariner.zig @@ -1,22 +1,18 @@ -//! chartstyle — MapLibre chart-style generation, client-side. A faithful 1:1 Zig -//! port of the C++ chartstyle/ module (chart_style.cpp + mariner.hpp), which is -//! itself a port of the Go web client's s52-style.mjs builders. +//! mariner — the S-52 mariner display settings and the builders that turn those +//! settings into MapLibre expressions. This is the settings model, not a style +//! generator: maplibre.zig assembles the full style.json and calls these builders +//! for the mariner-driven parts. //! -//! It PATCHES the mariner-driven parts of a MapLibre style template rather than -//! regenerating the whole style: SEABED01 depth shading, the sounding bold/faint -//! split (SNDFRM04), the danger-symbol safety swap (OBSTRN06/WRECKS05), contour -//! label units (SAFCON01), the per-scheme recolour (background/fills/lines/text + -//! halos/contour labels), and the client-side display filters AND-ed onto every -//! `source:"chart"` layer (category + M_QUAL, band, boundary/point style, INFORM01/ -//! CHDATD01 callout toggles, date validity, meta-bounds, text groups). +//! The settings drive SEABED01 depth shading, the sounding bold/faint split +//! (SNDFRM04), the danger-symbol safety swap (OBSTRN06/WRECKS05), contour label +//! units (SAFCON01), the per-scheme recolour (background/fills/lines/text + halos/ +//! contour labels), and the display filters AND-ed onto every `source:"chart"` +//! layer (category + M_QUAL, band, boundary/point style, INFORM01/CHDATD01 callout +//! toggles, date validity, meta-bounds, text groups). //! -//! Pure Zig (no libc): the host reads the template + colortables bytes and passes -//! them in; the C ABI wrapper lives in capi.zig. std.json.Value uses an -//! insertion-ordered ObjectMap, so the patched style keeps the template's key -//! order (the C++/nlohmann build alphabetises keys via std::map — semantically -//! identical, just a different serialisation). Colour `match` arms ARE emitted in -//! sorted-token order to mirror nlohmann's palette iteration, so they're byte-equal -//! to the C++ output. See ../../../chartstyle/src/chart_style.cpp for the oracle. +//! Pure Zig (no libc): the host passes in the template + colortables bytes; the C +//! ABI wrapper lives in capi.zig. Colour `match` arms are emitted in sorted-token +//! order so the output is byte-stable across builds. const std = @import("std"); @@ -27,15 +23,15 @@ const ObjectMap = std.json.ObjectMap; const M_TO_FT: f64 = 3.280839895; const FALLBACK = "#ff00ff"; -// ---- public model (mirrors chartstyle/include/chartstyle/mariner.hpp) ------- +// ---- public model -------------------------------------------------------- pub const Scheme = enum(c_int) { day = 0, dusk = 1, night = 2 }; pub const DepthUnit = enum(c_int) { meters = 0, feet = 1 }; pub const BoundaryStyle = enum(c_int) { symbolized = 0, plain = 1 }; // S-52 §8.6.1 -/// The S-52 mariner display options. Defaults match mariner.hpp (the Go web -/// client's defaults). -pub const MarinerSettings = struct { +/// The S-52 mariner display options. Defaults match the recreational web +/// client's defaults. +pub const Settings = struct { // -- colour scheme (S-52 day/dusk/night palette) -- scheme: Scheme = .day, @@ -90,13 +86,13 @@ pub const MarinerSettings = struct { // -- SCAMIN gating override (host ?ignoreScamin debug toggle) — NOT an S-52 // display setting; a render-time transport flag the C ABI carries through to - // the style builder. When true, styleJson drops SCAMIN scale-gating so every + // the style builder. When true, the style drops SCAMIN scale-gating so every // feature shows in-band regardless of its 1:N min-display-scale. Default off. ignore_scamin: bool = false, - // -- scamin-layers.md: gate SCAMIN with a live client-driven filter instead of - // per-value bucket layers — NOT an S-52 display setting; a render-time transport - // flag the C ABI carries to the style builder. When true, styleJson emits one + // -- gate SCAMIN with a live client-driven filter instead of per-value bucket + // layers — NOT an S-52 display setting; a render-time transport flag the C ABI + // carries to the style builder. When true, the style emits one // *_scamin layer per render-type (no minzoom buckets) and the client rewrites the // SCAMIN clause on boundary crossings. Default off = per-value buckets. scamin_filter_gate: bool = false, @@ -239,7 +235,7 @@ pub fn contourLabelColor(b: B, scheme: Scheme, palette: *const ObjectMap) !Value // DRVAL1/DRVAL2 vs the mariner's contours -> a depth colour token. Deepest band // first (first match in a `case` wins). `>= X && > X` on both bounds per spec. -pub fn seabedTokenExpr(b: B, m: *const MarinerSettings) !Value { +pub fn seabedTokenExpr(b: B, m: *const Settings) !Value { const d1 = try b.coalesce(try b.get("drval1"), b.int(-1)); const d2 = try b.coalesce(try b.get("drval2"), b.int(0)); const band = struct { @@ -275,7 +271,7 @@ pub fn seabedTokenExpr(b: B, m: *const MarinerSettings) !Value { // Fill colour for the `areas` layer: depth areas (carry drval1) shade live via // SEABED01; everything else uses its baked colour token. -pub fn areasFillColor(b: B, palette: *const ObjectMap, m: *const MarinerSettings) !Value { +pub fn areasFillColor(b: B, palette: *const ObjectMap, m: *const Settings) !Value { return b.arr(&.{ b.s("case"), try b.arr(&.{ b.s("has"), b.s("drval1") }), @@ -291,7 +287,7 @@ pub fn areasFillColor(b: B, palette: *const ObjectMap, m: *const MarinerSettings // metres); when the mariner selects feet, the displayed digits come from the baked // whole-feet glyph variant (sym_s_ft/sym_g_ft) instead — a recreational unit option, // not ECDIS (see scene.appendSoundingProps), mirroring contourLabelField. -pub fn soundingsIconImage(b: B, m: *const MarinerSettings) !Value { +pub fn soundingsIconImage(b: B, m: *const Settings) !Value { const ss = if (m.depth_unit == .feet) "sym_s_ft" else "sym_s"; const sg = if (m.depth_unit == .feet) "sym_g_ft" else "sym_g"; const depthLE = try b.arr(&.{ b.s("<="), try b.coalesce(try b.get("depth"), b.int(0)), try b.flt(m.safety_depth) }); @@ -305,7 +301,7 @@ pub fn soundingsIconImage(b: B, m: *const MarinerSettings) !Value { // OBSTRN06/WRECKS05: a danger symbol deeper than the live safety contour swaps to // the less-prominent DANGER02 (sym_deep). pivot_center draws the "ctr:" variant. -pub fn pointSymbolImage(b: B, m: *const MarinerSettings) !Value { +pub fn pointSymbolImage(b: B, m: *const Settings) !Value { const name = try b.arr(&.{ b.s("case"), try b.arr(&.{ @@ -329,7 +325,7 @@ pub fn pointSymbolImage(b: B, m: *const MarinerSettings) !Value { // up. A 2 m contour reads "6" ft (6.56 truncated), not "6.5" or "7": a depth always // errs shallow (toward the surface), the safe direction, matching SNDFRM04's whole-feet // truncation. -pub fn contourLabelField(b: B, m: *const MarinerSettings) !Value { +pub fn contourLabelField(b: B, m: *const Settings) !Value { const v = if (m.depth_unit == .feet) try b.arr(&.{ b.s("floor"), try b.arr(&.{ b.s("*"), try b.get("valdco"), try b.flt(M_TO_FT) }) }) else @@ -345,7 +341,7 @@ pub fn contourLabelField(b: B, m: *const MarinerSettings) !Value { // ---- client-side display filters ------------------------------------------- // Display category (S-52 §10.3.4) + M_QUAL data-quality overlay. -pub fn categoryFilter(b: B, m: *const MarinerSettings) !Value { +pub fn categoryFilter(b: B, m: *const Settings) !Value { var en = Array.init(b.a); if (m.display_base) try en.append(b.int(0)); if (m.display_standard) try en.append(b.int(1)); @@ -372,7 +368,7 @@ pub fn bandFilter(b: B, enabled: []const i32) !Value { } // Boundary symbolization (S-52 §8.6.1): show common (2) + the active style. -pub fn boundaryFilter(b: B, m: *const MarinerSettings) !Value { +pub fn boundaryFilter(b: B, m: *const Settings) !Value { const rank: i64 = if (m.boundary_style == .plain) 0 else 1; return b.arr(&.{ b.s("in"), @@ -382,7 +378,7 @@ pub fn boundaryFilter(b: B, m: *const MarinerSettings) !Value { } // Point-symbol style (S-52 §11.2.2): show common (2) + the active style. -pub fn pointStyleFilter(b: B, m: *const MarinerSettings) !Value { +pub fn pointStyleFilter(b: B, m: *const Settings) !Value { const rank: i64 = if (m.simplified_points) 1 else 0; return b.arr(&.{ b.s("in"), @@ -392,7 +388,7 @@ pub fn pointStyleFilter(b: B, m: *const MarinerSettings) !Value { } // S-52 §14.5 text-group selection. Important text (11) is always on. -pub fn textGroupFilter(b: B, m: *const MarinerSettings) !Value { +pub fn textGroupFilter(b: B, m: *const Settings) !Value { const g = try b.coalesce(try b.get("tgrp"), b.int(-1)); const namedSet = struct { fn make(bb: B) !Value { @@ -422,7 +418,7 @@ pub fn textGroupFilter(b: B, m: *const MarinerSettings) !Value { // (unbanded — always shown) OR its `vg` is NOT in the off-set, so any group the // host didn't list stays visible. Byte-identical to the host's s52-style.mjs // expression, so whichever backend builds the style produces the same filter. -pub fn viewingGroupFilter(b: B, m: *const MarinerSettings) !?Value { +pub fn viewingGroupFilter(b: B, m: *const Settings) !?Value { const off = m.viewing_groups_off orelse return null; if (off.len == 0) return null; var en = Array.init(b.a); @@ -485,7 +481,7 @@ pub fn dateFilter(b: B, today_str: []const u8) !Value { // The viewing date "YYYYMMDD": the mariner's pinned date if set, else `now_unix` // (Unix epoch seconds, supplied by the host) rendered as a UTC calendar date. -pub fn viewingDate(b: B, m: *const MarinerSettings, now_unix: i64) ![]const u8 { +pub fn viewingDate(b: B, m: *const Settings, now_unix: i64) ![]const u8 { if (m.date_view.len == 8) { var digits = true; for (m.date_view) |c| digits = digits and (c >= '0' and c <= '9'); @@ -507,7 +503,7 @@ pub fn viewingDate(b: B, m: *const MarinerSettings, now_unix: i64) ![]const u8 { // so it is NOT included here. Used by the single-pass style builder (style.zig) to // compose each layer's filter inline — the consolidation that retired buildStyle's // template-patch pass. Allocated in `a` (caller's arena). -pub fn commonChartFilters(a: std.mem.Allocator, m: *const MarinerSettings, enabled_bands: ?[]const i32, now_unix: i64) ![]Value { +pub fn commonChartFilters(a: std.mem.Allocator, m: *const Settings, enabled_bands: ?[]const i32, now_unix: i64) ![]Value { const b = B{ .a = a }; var clauses = Array.init(a); try clauses.append(try categoryFilter(b, m)); diff --git a/src/assets/assets.zig b/src/style/style.zig similarity index 77% rename from src/assets/assets.zig rename to src/style/style.zig index 3068dea..e9787ca 100644 --- a/src/assets/assets.zig +++ b/src/style/style.zig @@ -1,40 +1,40 @@ -//! assets — offline portrayal-asset generation for the chart bundle. Mirrors the -//! Go oracle's internal/engine/assets (EmitS101): the rendering half of the -//! tile/style contract, emitted from the same S-101 catalogue that drives the -//! tiles so the two can't drift. Mirrors the Go oracle's bundle assets. +//! style — MapLibre style generation for the chart tiles: color tables, line +//! styles, and the style.json layer set, plus the S-52 mariner expression +//! builders. The style and the tiles come from the same S-101 catalogue, so the +//! two stay in sync. //! //! Pure Zig (no libc/fs): callers read the catalogue bytes and pass them in, the -//! same shape as s100/catalogue.zig. RGB lives ONLY in colortables.json; the -//! tiles stay colour *tokens*. +//! same shape as s101/catalogue.zig. RGB lives only in the color tables; the +//! tiles carry color *tokens*. //! -//! Implemented now: colortables.json (token -> hex, per day/dusk/night palette), -//! the MapLibre style.json layer set (style.zig), and manifest.json (pins -//! schema_version; couples tiles <-> portrayal). TODO, tracked in -//! next: linestyles.json, sprite/pattern atlases (SVG raster), -//! and glyphs (SDF) — to light up the symbol/text/pattern layers. +//! * color tables — token -> hex, one per day/dusk/night palette +//! * the MapLibre style.json layer set (maplibre.zig) +//! * line styles and the S-52 mariner expression builders (mariner.zig) const std = @import("std"); -/// The tile-vocabulary version both halves of a bundle are stamped with: the MVT -/// layer/property set in scene.zig and the style/colortables that render it. -/// Bump on ANY change to layer names or feature property keys. tile57/2 = the -/// 6-source-layer schema (the `_scamin` twins folded into their base; SCAMIN is a -/// per-feature `scamin` property the style gates band-independently). +/// The tile-vocabulary version both the tiles and the style are stamped with: +/// the MVT layer/property set in scene.zig and the style/color tables that +/// render it. Bump on any change to layer names or feature property keys. +/// tile57/2 = the 6-source-layer schema (the `_scamin` twins folded into their +/// base; SCAMIN is a per-feature `scamin` property the style gates +/// band-independently). pub const SCHEMA_VERSION = "tile57/2"; -// MapLibre style.json generation lives in style.zig. -pub const StyleOpts = @import("style.zig").StyleOpts; -pub const styleJson = @import("style.zig").styleJson; -pub const displayDenom = @import("style.zig").displayDenom; -pub const displayDenomZ = @import("style.zig").displayDenomZ; -pub const scaminGateK = @import("style.zig").scaminGateK; -pub const buildFromTemplate = @import("style.zig").buildFromTemplate; -pub const buildFromTemplateScamin = @import("style.zig").buildFromTemplateScamin; -pub const styleDiff = @import("style.zig").styleDiff; - -/// The S-52 mariner expression builders (MapLibre style patching) — folded -/// in: assets is its only sibling and every consumer wants both. -pub const chartstyle = @import("chartstyle.zig"); +// MapLibre style.json generation lives in maplibre.zig. +pub const Options = @import("maplibre.zig").Options; +pub const json = @import("maplibre.zig").json; +pub const displayDenom = @import("maplibre.zig").displayDenom; +pub const displayDenomZ = @import("maplibre.zig").displayDenomZ; +pub const scaminGateK = @import("maplibre.zig").scaminGateK; +pub const buildFromTemplate = @import("maplibre.zig").buildFromTemplate; +pub const buildFromTemplateScamin = @import("maplibre.zig").buildFromTemplateScamin; +pub const diff = @import("maplibre.zig").diff; + +/// The S-52 mariner display settings model (`mariner.Settings`) and the builders +/// that encode those settings as MapLibre expressions. maplibre.zig calls these +/// for the mariner-driven parts of the style. +pub const mariner = @import("mariner.zig"); // ---- colortables.json ---------------------------------------------------- @@ -406,54 +406,6 @@ pub fn linestylesJson(alloc: std.mem.Allocator, srcs: []const LineStyleSrc) ![]u return out.toOwnedSlice(alloc); } -// ---- manifest.json ------------------------------------------------------- - -/// Inputs for the bundle manifest. Relative paths only (the bundle is -/// relocatable). bbox is [west, south, east, north]; anchor is [lon, lat]. -pub const Manifest = struct { - generator: []const u8, - created: []const u8 = "", // ISO 8601; Zig has no wall clock, so passed in - catalogue_version: []const u8 = "", - tiles_rel: []const u8, - colortables_rel: []const u8, - minzoom: u8, - maxzoom: u8, - bbox: [4]f64, - anchor: [2]f64, - cells: []const []const u8, - styles: ?Styles = null, // per-palette style.json paths, if emitted - - pub const Styles = struct { day: []const u8, dusk: []const u8, night: []const u8 }; -}; - -/// Emit the bundle manifest.json. Loaded first by a renderer, which refuses a -/// bundle whose schema_version it doesn't speak — turning tile/style coupling -/// into a checked invariant. Returns allocator-owned bytes. -pub fn manifestJson(alloc: std.mem.Allocator, m: Manifest) ![]u8 { - // The manifest is plain data: describe it as a value and let std.json emit it. - // indent_2 keeps it human-readable; emit_null_optional_fields drops "styles" - // (the one optional) when absent. - return std.json.Stringify.valueAlloc(alloc, .{ - .bundle_version = 1, - .schema_version = SCHEMA_VERSION, - .generator = m.generator, - .created = m.created, - .catalogue_version = m.catalogue_version, - .data = .{ - .tiles = m.tiles_rel, - .minzoom = m.minzoom, - .maxzoom = m.maxzoom, - .bbox = m.bbox, - .anchor = m.anchor, - .cells = m.cells, - }, - .portrayal = .{ - .colortables = m.colortables_rel, - .styles = m.styles, - }, - }, .{ .whitespace = .indent_2, .emit_null_optional_fields = false }); -} - // ---- tests --------------------------------------------------------------- test "colorTablesJson: sorted tokens, lowercase hex, all three palettes" { @@ -515,35 +467,7 @@ test "linestylesJson: dash pattern + placed symbols, sorted ids, skips no-interv try std.testing.expectEqualStrings(expected, out); } -test "manifestJson: pins schema_version and couples tiles to portrayal" { - const out = try manifestJson(std.testing.allocator, .{ - .generator = "tile57 0.1.0", - .created = "2026-06-27T00:00:00Z", - .catalogue_version = "S-101 PC 1.4.0", - .tiles_rel = "tiles/chart.pmtiles", - .colortables_rel = "assets/colortables.json", - .minzoom = 8, - .maxzoom = 16, - .bbox = .{ -76.55, 38.90, -76.40, 39.02 }, - .anchor = .{ -76.475, 38.96 }, - .cells = &.{ "US5MD1MC", "US4MD81M" }, - }); - defer std.testing.allocator.free(out); - const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, out, .{}); - defer parsed.deinit(); - const o = parsed.value.object; - try std.testing.expectEqualStrings("tile57/2", o.get("schema_version").?.string); - const data = o.get("data").?.object; - try std.testing.expectEqualStrings("tiles/chart.pmtiles", data.get("tiles").?.string); - try std.testing.expectEqual(@as(usize, 2), data.get("cells").?.array.items.len); - try std.testing.expectEqualStrings("US5MD1MC", data.get("cells").?.array.items[0].string); - const portrayal = o.get("portrayal").?.object; - try std.testing.expectEqualStrings("assets/colortables.json", portrayal.get("colortables").?.string); - // "styles" is omitted when not provided (emit_null_optional_fields = false) - try std.testing.expect(portrayal.get("styles") == null); -} - test { - _ = chartstyle; - _ = @import("style.zig"); // run style.zig's styleJson / buildFromTemplate tests too + _ = mariner; + _ = @import("maplibre.zig"); // run the style.json builder tests too } diff --git a/src/tile57.zig b/src/tile57.zig index 5fa2bb4..18cd324 100644 --- a/src/tile57.zig +++ b/src/tile57.zig @@ -1,74 +1,129 @@ -//! tile57 — the public Zig API for the S-57 → MVT vector-tile + S-52 style engine. +//! tile57 — the public Zig API for turning S-57 ENC data into vector tiles. //! -//! tile57 turns IHO S-57 ENC cells into Mapbox Vector Tiles plus a MapLibre -//! S-52 style and its portrayal assets (colour tables, line styles, sprite + -//! pattern atlases). It is high-performance and low-memory by design: cells are -//! indexed cheaply and parsed/portrayed lazily per requested tile, multi-cell -//! bakes stream band-by-band, and the foundational format/encode packages are -//! pure Zig (no libc). +//! The pipeline: parse IHO S-57 charts (ENC cells), apply S-101 portrayal, and +//! bake each chart to its own MLT vector tiles in a PMTiles archive at its +//! compilation scale. A runtime compositor then stitches the per-chart archives +//! into one tile for any (z, x, y) on demand, using an ownership partition so +//! charts never double-draw where they meet. The same engine renders a chart to +//! PNG or PDF, and generates the +//! MapLibre S-52 style plus its portrayal assets (color tables, line styles, +//! sprite and pattern atlases). //! //! This module is the curated public surface. Add it as a dependency and -//! `@import("tile57")`. The C ABI in include/tile57.h is a thin shim over the -//! same Zig API (see capi.zig). +//! `@import("tile57")`. The C ABI in include/tile57.h is a thin shim over the same +//! Zig API (see capi.zig). Consumers that want only one layer can depend on that +//! module directly — `iso8211`, `s57`, `s101`, `tiles`, `geometry`, `coverage`, +//! `compose`, `render`, and `style` are each standalone. //! //! Surface: -//! - High-level engine: `Chart` (open → tile), `bakeArchive`, `buildStyle` [Phase 2] -//! - Portrayal assets: `assets` (colortables, linestyles, sprite/pattern) -//! - Style patching: `chartstyle` -//! - Tiling: `mvt`, `tile`, `pmtiles`, `bake_enc`, `scene` -//! - Raw formats: `formats.{iso8211, s57, s100}` +//! - Chart: `Chart` — open, then render / query / inspect +//! - Bake: `bake` — a chart (or a tree of charts) to per-chart PMTiles +//! - Compose: `compose` — per-chart archives + `partition` -> tiles on demand +//! - Style: `style` — the MapLibre style; `sprite` — the atlases +//! - Tiling: `mvt`, `mlt`, `tile`, `pmtiles`, `gzip`, `band`, `scene` +//! - Render: `render` — the Surface/Canvas rendering path +//! - Formats: `formats.{iso8211, s57, s101}`, `coverage` const std = @import("std"); /// Library version (matches build.zig.zon and tile57_version()). -pub const version = "0.1.0"; +pub const version = "0.3.0"; -// ---- high-level engine: open a Chart, fetch tiles, bake, build a style ---- +// ---- Chart: open a chart, then render / query / inspect -------------------- const chart = @import("chart.zig"); -/// An embeddable chart tile source: `Chart.openBytes` / `Chart.openCells`, -/// then `.tile(z,x,y)`. See chart.zig. +/// An embeddable chart source: open from a path, from bytes, or as a multi-chart +/// ENC_ROOT (streaming), then render / query / inspect. See chart.zig. pub const Chart = chart.Chart; pub const Format = chart.Format; -pub const CellInput = chart.CellInput; +pub const ChartInput = chart.ChartInput; pub const Progress = chart.Progress; -// Streaming ENC_ROOT open (read cell bytes on demand, low memory): see -// Chart.openCellsStreaming. -pub const CellMeta = chart.CellMeta; -pub const CellBytes = chart.CellBytes; -pub const CellReadFn = chart.CellReadFn; -/// Bake an ENC_ROOT into one band-streamed PMTiles archive. -pub const bakeArchive = chart.bakeArchive; -/// Free bytes returned by `Chart.tile` / `bakeArchive`. +/// Streaming ENC_ROOT open (read a chart's bytes on demand, low memory): see +/// Chart.openChartsStreaming. +pub const ChartMeta = chart.ChartMeta; +pub const ChartBytes = chart.ChartBytes; +pub const ChartReadFn = chart.ChartReadFn; +/// Free bytes returned by the render / bake entry points. pub const freeBytes = chart.freeBytes; +/// Warm the embedded S-101 catalogue + Lua rules once, before serving. +pub const warmup = chart.warmup; -/// MapLibre style generation from a template + mariner S-52 display settings. -/// `build` is the single style builder (regenerates the full style with the mariner -/// baked in; the old template-patch pass is retired) — see assets.buildFromTemplate. -pub const style = struct { - pub const build = assets.buildFromTemplate; - pub const Mariner = chartstyle.MarinerSettings; +// ---- Bake: charts -> per-chart PMTiles archives ---------------------------- +/// Bake each chart to its own PMTiles at its compilation scale — the input the +/// compositor serves from. Strictly one chart, one archive. +pub const bake = struct { + pub const chartBytes = chart.bakeChartBytes; // one chart + updates -> PMTiles bytes + pub const chartsParallel = chart.bakeChartsParallel; // N charts -> N archives, threaded + pub const chartsToFiles = chart.bakeChartsToFiles; // N charts -> files under a dir + pub const tree = chart.bakeTree; // walk an ENC_ROOT, bake each chart to a mirrored path + pub const pmtilesMetadata = chart.pmtilesMetadata; // read an archive's TileJSON metadata + pub const Progress = chart.BakeProgress; }; -// ---- portrayal asset + style generation ---------------------------------- -pub const assets = @import("assets"); // colortables / linestyles / style / manifest -pub const sprite = @import("sprite"); // S-101 sprite + area-fill pattern atlases -pub const chartstyle = @import("assets").chartstyle; // mariner-driven MapLibre style patching +// ---- Compose: per-chart archives + a partition -> any output on demand ----- +/// The runtime compositor: a `ComposeSource` (open with `ComposeSource.open` / +/// `ComposeSource.openFiles`) over per-chart archives + a partition offers the +/// SAME outputs as a Chart, composed — `ComposeSource.tile` for one tile, +/// `renderView` / `renderSurfaceView` / `queryPoint` for composed views and the +/// composed pick. (The view calls are implemented beside Chart — the underlying +/// `compose` module is a dependency leaf without the render path — and surfaced +/// here under the compose name they belong to.) +pub const compose = struct { + const mod = @import("compose"); + pub const ComposeSource = mod.ComposeSource; + pub const ChartArchive = mod.ChartArchive; + pub const TileResult = mod.TileResult; + pub const LoadedCov = mod.LoadedCov; + pub const tile = mod.composeTile; // the stateless core ComposeSource.tile uses + pub const toPlaneCells = mod.toPlaneCells; + pub const clip = mod.clip; + /// The composed view render — PNG, PDF, or a callback canvas. + pub const renderView = chart.renderComposeView; + /// The composed world-space surface stream (the GPU vector twin). + pub const renderSurfaceView = chart.renderComposeSurfaceView; + /// The composed cursor pick (S-52 §10.8, across chart boundaries). + pub const queryPoint = chart.composeQueryPoint; +}; +/// The ownership partition and its `.tpart` sidecar (serialize / deserialize). +pub const partition = @import("geometry").partition; +/// The integer computational geometry the compositor and baker share. +pub const geometry = @import("geometry"); + +// ---- Style + portrayal assets ---------------------------------------------- +/// MapLibre style generation: `style.json` builds a style.json, `style.Options` +/// its inputs, `style.mariner` the S-52 display settings, `style.diff` the minimal +/// mutation to retint/refilter, plus the color tables and line styles. +pub const style = @import("style"); +/// The S-52 mariner display settings model (`style.mariner.Settings`). +pub const Mariner = style.mariner.Settings; +/// S-101 sprite + area-fill pattern atlases (SVG raster). +pub const sprite = @import("sprite"); -// ---- tiling / encoding --------------------------------------------------- +// ---- Tiling / encoding ----------------------------------------------------- pub const mvt = @import("tiles").mvt; // Mapbox Vector Tile encode/decode +pub const mlt = @import("tiles").mlt; // MapLibre Tile encode/decode (the bake default) pub const tile = @import("tiles").tile; // web-mercator tiling + clipping pub const pmtiles = @import("tiles").pmtiles; // PMTiles read/write -pub const bake_enc = @import("scene").bake_enc; // banded multi-cell ENC_ROOT → PMTiles -pub const scene = @import("scene"); // S-57 feature → MVT tile +pub const gzip = @import("tiles").gzip; // gzip (tile payloads, PMTiles internals) +pub const band = @import("tiles").band; // compilation-scale -> zoom-range mapping +pub const scene = @import("scene"); // S-57 + portrayal -> tile surface +pub const bake_enc = @import("scene").bake_enc; // the banded chart baker + +// ---- Render surfaces ------------------------------------------------------- +/// The Surface/Canvas rendering path: PNG raster, vector PDF, ASCII, and the +/// callback surfaces a GPU host drives. +pub const render = @import("render"); -// ---- raw S-57 / S-100 formats (advanced) --------------------------------- +// ---- Raw formats (advanced) ------------------------------------------------ pub const formats = struct { - pub const iso8211 = @import("s57").iso8211; // ISO/IEC 8211 records - pub const s57 = @import("s57"); // S-57 ENC cell parser + geometry - pub const s100 = @import("s100"); // S-100/S-101 catalogue + adaptation + pub const iso8211 = @import("s57").iso8211; // ISO/IEC 8211 container records + pub const s57 = @import("s57"); // the S-57 chart parser + geometry + pub const s101 = @import("s101"); // S-101 catalogue + adapter + instructions }; +/// Per-chart M_COVR coverage sidecar (carried in an archive's PMTiles metadata). +pub const coverage = @import("coverage"); test { std.testing.refAllDecls(@This()); std.testing.refAllDecls(formats); + std.testing.refAllDecls(bake); } diff --git a/src/tiles/band.zig b/src/tiles/band.zig new file mode 100644 index 0000000..ec723f6 --- /dev/null +++ b/src/tiles/band.zig @@ -0,0 +1,70 @@ +//! Navigational-purpose bands: the map from an S-57 cell's compilation scale to +//! the Web-Mercator zoom range it serves. Pure integer math, shared by the baker +//! (which bakes each cell over its band's zooms) and the compositor (which reads +//! the band to decide overscale fill-up), so neither owns the mapping. + +/// Native [minzoom, maxzoom] Web-Mercator span for a navigational-purpose band. +pub const ZoomRange = struct { min: u8, max: u8 }; + +/// Navigational-purpose bands, finest -> coarsest (the order bands must be baked +/// in for best-band dedup). +pub const Band = enum(u8) { berthing = 0, harbor, approach, coastal, general, overview }; + +/// All bands finest -> coarsest (the bake call order). +pub const bands_fine_to_coarse = [_]Band{ .berthing, .harbor, .approach, .coastal, .general, .overview }; + +/// Map a compilation-scale denominator (CSCL, 1:N) to its band. +pub fn bandOf(cscl: i32) Band { + const n: i64 = if (cscl <= 0) 50_000 else cscl; + if (n <= 8_000) return .berthing; + if (n <= 32_000) return .harbor; + if (n <= 130_000) return .approach; + if (n <= 500_000) return .coastal; + if (n <= 2_300_000) return .general; + return .overview; +} + +/// Overscale fill-up depth DEFAULT: how many zooms past its native max a band's +/// own cells keep baking (only where nothing finer already emitted). Every +/// extension zoom ~4x that band's tile count over its uncovered footprint +/// (measured: +2 turned a 5.6k-tile approach pass into 41k), so the default is +/// ONE crisp overscale zoom; TILE57_FILLUP_DZ=0..2 overrides per bake +/// (Baker.fillup_dz). 0 never blanks — the client camera stops at the probed +/// data depth and MapLibre stretches one level past it. +pub const FILLUP_DZ: u8 = 1; + +/// Absolute fill-up ceiling: extension zooms never exceed this. The fill-up +/// serves the MID-ZOOM seam where a coarse chart is the finest coverage (the +/// blank bay at z12-15); letting fine bands extend too (harbor->z17-18, +/// berthing->z19-20) quadruples the tile count per extra zoom across every +/// harbor footprint for content nobody needs — a district pack ballooned from +/// ~800k to 13M+ planned tiles. A band's NATIVE window is never clamped by this; +/// past its data the camera stops at the probed depth instead. +pub const FILLUP_CEIL: u8 = 15; + +/// A band's native zoom span. Adjacent bands overlap by one zoom; best-band dedup +/// resolves the overlap to the finer band. +pub fn bandZooms(band: Band) ZoomRange { + return switch (band) { + .berthing => .{ .min = 16, .max = 18 }, + .harbor => .{ .min = 13, .max = 16 }, + .approach => .{ .min = 11, .max = 13 }, + .coastal => .{ .min = 9, .max = 11 }, + .general => .{ .min = 7, .max = 9 }, + .overview => .{ .min = 0, .max = 7 }, + }; +} + +test "bandOf maps compilation scale to band" { + const std = @import("std"); + try std.testing.expectEqual(Band.harbor, bandOf(20_000)); + try std.testing.expectEqual(Band.approach, bandOf(50_000)); + try std.testing.expectEqual(Band.overview, bandOf(3_000_000)); + try std.testing.expectEqual(Band.approach, bandOf(0)); // unknown -> 50k default +} + +test "bandZooms is finest-to-coarsest with one-zoom overlap" { + const std = @import("std"); + try std.testing.expectEqual(ZoomRange{ .min = 11, .max = 13 }, bandZooms(.approach)); + try std.testing.expectEqual(ZoomRange{ .min = 9, .max = 11 }, bandZooms(.coastal)); +} diff --git a/src/tiles/filemap.zig b/src/tiles/filemap.zig new file mode 100644 index 0000000..577ac29 --- /dev/null +++ b/src/tiles/filemap.zig @@ -0,0 +1,57 @@ +//! filemap — a read-only memory map of a file, cross-platform. +//! +//! Zig std has no portable mmap: `std.posix.mmap` is POSIX-only (its PROT/MAP flag +//! types are `void` on Windows, so it won't even compile there), and `std.os.windows` +//! doesn't bind the file-mapping calls. So this maps via `std.posix.mmap` on POSIX and +//! `CreateFileMapping`/`MapViewOfFile` (declared below) on Windows — the SAME lazily +//! paged, page-cache-shared view on both, so a whole chart library can be open without +//! being resident. Callers pass a file handle (`std.posix.fd_t` is `HANDLE` on Windows) +//! and a length; the mapping outlives the file handle on both platforms. +const std = @import("std"); +const builtin = @import("builtin"); +const windows = std.os.windows; +const page = std.heap.page_size_min; + +const PAGE_READONLY: windows.DWORD = 0x02; +const FILE_MAP_READ: windows.DWORD = 0x0004; +extern "kernel32" fn CreateFileMappingW( + hFile: windows.HANDLE, + lpAttributes: ?*anyopaque, + flProtect: windows.DWORD, + dwMaximumSizeHigh: windows.DWORD, + dwMaximumSizeLow: windows.DWORD, + lpName: ?windows.LPCWSTR, +) callconv(.winapi) ?windows.HANDLE; +extern "kernel32" fn MapViewOfFile( + hFileMappingObject: windows.HANDLE, + dwDesiredAccess: windows.DWORD, + dwFileOffsetHigh: windows.DWORD, + dwFileOffsetLow: windows.DWORD, + dwNumberOfBytesToMap: usize, +) callconv(.winapi) ?windows.LPVOID; +extern "kernel32" fn UnmapViewOfFile(lpBaseAddress: windows.LPCVOID) callconv(.winapi) windows.BOOL; + +/// Map the first `len` bytes of `handle` read-only (`len` must be > 0). Release with +/// `unmap`. The file handle may be closed once this returns — the view keeps the +/// underlying data alive on both POSIX (mmap) and Windows (MapViewOfFile). +pub fn mapReadonly(handle: std.posix.fd_t, len: usize) error{IoFailed}![]align(page) const u8 { + if (builtin.os.tag == .windows) { + const h = CreateFileMappingW(handle, null, PAGE_READONLY, 0, 0, null) orelse return error.IoFailed; + defer windows.CloseHandle(h); // the mapped view keeps the section alive after this + const p = MapViewOfFile(h, FILE_MAP_READ, 0, 0, 0) orelse return error.IoFailed; + // MapViewOfFile is aligned to the 64 KB allocation granularity, so >= page align. + const base: [*]align(page) const u8 = @ptrCast(@alignCast(p)); + return base[0..len]; + } + return std.posix.mmap(null, len, .{ .READ = true }, .{ .TYPE = .PRIVATE }, handle, 0) catch + return error.IoFailed; +} + +/// Release a mapping returned by `mapReadonly`. +pub fn unmap(m: []align(page) const u8) void { + if (builtin.os.tag == .windows) { + _ = UnmapViewOfFile(@ptrCast(m.ptr)); + } else { + std.posix.munmap(m); + } +} diff --git a/src/tiles/mlt.zig b/src/tiles/mlt.zig index b9ade3e..4323773 100644 --- a/src/tiles/mlt.zig +++ b/src/tiles/mlt.zig @@ -638,14 +638,16 @@ fn unzigzag32(v: u64) i32 { return @bitCast((u >> 1) ^ (0 -% (u & 1))); } -const StreamMeta = struct { phys: u8, sub: u8, num_values: u64, byte_length: u64 }; +// num_values / byte_length count and address in-memory data, so they are usize +// (u32 on 32-bit targets); the wire varints are range-checked into them here. +const StreamMeta = struct { phys: u8, sub: u8, num_values: usize, byte_length: usize }; fn readStreamMeta(r: *DecReader) StreamMeta { const b0 = r.byte(); _ = r.byte(); // llt/plt byte — the subset's streams are self-describing by (phys, sub) const nv = r.varint(); const bl = r.varint(); - return .{ .phys = b0 >> 4, .sub = b0 & 0x0F, .num_values = nv, .byte_length = bl }; + return .{ .phys = b0 >> 4, .sub = b0 & 0x0F, .num_values = @intCast(nv), .byte_length = @intCast(bl) }; } // ORC byte-RLE (as writePresentStream emits, plus repeat runs for safety): @@ -691,14 +693,14 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { var layers = std.ArrayList(mvt.DecodedLayer).empty; var top = DecReader{ .buf = data }; while (top.pos < data.len) { - const block_len = top.varint(); + const block_len: usize = @intCast(top.varint()); const block = top.bytes(block_len); var r = DecReader{ .buf = block }; if (r.varint() != 1) return error.BadTile; // tag - const name = try a.dupe(u8, r.bytes(r.varint())); + const name = try a.dupe(u8, r.bytes(@intCast(r.varint()))); const extent: u32 = @intCast(r.varint()); - const ncols = r.varint(); + const ncols: usize = @intCast(r.varint()); if (ncols == 0 or r.byte() != TYPECODE_GEOMETRY) return error.BadTile; var cols = std.ArrayList(DecCol).empty; for (1..ncols) |_| { @@ -713,16 +715,16 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { ST_FLOAT => .float, else => return error.BadTile, }; - const key = try a.dupe(u8, r.bytes(r.varint())); + const key = try a.dupe(u8, r.bytes(@intCast(r.varint()))); try cols.append(a, .{ .kind = kind, .nullable = (tc - 10) % 2 == 1, .key = key }); } // ---- geometry column ---------------------------------------------- - const n_gstreams = r.varint(); + const n_gstreams: usize = @intCast(r.varint()); var gtypes: []u32 = &.{}; - var geoms: []u64 = &.{}; - var parts_lens: []u64 = &.{}; - var rings_lens: []u64 = &.{}; + var geoms: []usize = &.{}; + var parts_lens: []usize = &.{}; + var rings_lens: []usize = &.{}; var verts: []i32 = &.{}; for (0..n_gstreams) |si| { const m = readStreamMeta(&r); @@ -731,14 +733,14 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { gtypes = try a.alloc(u32, m.num_values); for (gtypes) |*g| g.* = @intCast(r.varint()); } else if (m.phys == PHYS_LENGTH and m.sub == LEN_GEOMETRIES) { - geoms = try a.alloc(u64, m.num_values); - for (geoms) |*v| v.* = r.varint(); + geoms = try a.alloc(usize, m.num_values); + for (geoms) |*v| v.* = @intCast(r.varint()); } else if (m.phys == PHYS_LENGTH and m.sub == LEN_PARTS) { - parts_lens = try a.alloc(u64, m.num_values); - for (parts_lens) |*v| v.* = r.varint(); + parts_lens = try a.alloc(usize, m.num_values); + for (parts_lens) |*v| v.* = @intCast(r.varint()); } else if (m.phys == PHYS_LENGTH and m.sub == LEN_RINGS) { - rings_lens = try a.alloc(u64, m.num_values); - for (rings_lens) |*v| v.* = r.varint(); + rings_lens = try a.alloc(usize, m.num_values); + for (rings_lens) |*v| v.* = @intCast(r.varint()); } else { // VertexBuffer (componentwise delta + zigzag) verts = try a.alloc(i32, m.num_values); var px: i32 = 0; @@ -763,7 +765,7 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { for (gtypes, 0..) |g, fi| { switch (g) { G_POINT, G_MULTIPOINT => { - const n: u64 = if (g == G_POINT) 1 else blk: { + const n: usize = if (g == G_POINT) 1 else blk: { const v = geoms[gi]; gi += 1; break :blk v; @@ -778,7 +780,7 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { feats[fi] = .{ .geom_type = .point, .parts = parts, .properties = &.{} }; }, G_MULTILINESTRING, G_LINESTRING => { - const nlines: u64 = if (g == G_MULTILINESTRING) blk: { + const nlines: usize = if (g == G_MULTILINESTRING) blk: { const v = geoms[gi]; gi += 1; break :blk v; @@ -831,8 +833,8 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { } if (nstreams == 3) { // dictionary const lm = readStreamMeta(&r); - const dlens = try a.alloc(u64, lm.num_values); - for (dlens) |*v| v.* = r.varint(); + const dlens = try a.alloc(usize, lm.num_values); + for (dlens) |*v| v.* = @intCast(r.varint()); const dm = readStreamMeta(&r); _ = dm; const dict = try a.alloc([]const u8, dlens.len); @@ -840,15 +842,15 @@ pub fn decode(a: std.mem.Allocator, data: []const u8) ![]mvt.DecodedLayer { const om = readStreamMeta(&r); var fi: usize = 0; for (0..om.num_values) |_| { - const ix = r.varint(); + const ix: usize = @intCast(r.varint()); while (present != null and !present.?[fi]) fi += 1; try propbuf[fi].append(a, .{ .key = col.key, .value = .{ .string = dict[ix] } }); fi += 1; } } else { // plain: lengths + data const lm = readStreamMeta(&r); - const lens = try a.alloc(u64, lm.num_values); - for (lens) |*v| v.* = r.varint(); + const lens = try a.alloc(usize, lm.num_values); + for (lens) |*v| v.* = @intCast(r.varint()); _ = readStreamMeta(&r); var fi: usize = 0; for (lens) |sl| { diff --git a/src/tiles/mvt.zig b/src/tiles/mvt.zig index e715162..707d860 100644 --- a/src/tiles/mvt.zig +++ b/src/tiles/mvt.zig @@ -572,3 +572,152 @@ test "zigzag" { try std.testing.expectEqual(@as(i64, -1), unzig(zigzag(-1))); try std.testing.expectEqual(@as(i64, 12345), unzig(zigzag(12345))); } + +// ---- tile schema + polygon ring winding ------------------------------------ + +/// The MVT source-layer set of the tile57/2 schema, in emit order. The scene +/// emitter fills these layers and the compositor stitches them by name. +pub const VECTOR_LAYERS = [_][]const u8{ + "areas", "area_patterns", "lines", "point_symbols", "soundings", "text", +}; + +/// Shoelace signed area (x2) of a ring in tile space; only its sign is used. +/// y is down, so a positive value is a clockwise (exterior) ring per the MVT spec. +fn ringSignedArea(ring: []const Point) i64 { + if (ring.len < 3) return 0; + var area: i64 = 0; + var j: usize = ring.len - 1; + for (ring, 0..) |p, i| { + const q = ring[j]; + area += @as(i64, q.x) * @as(i64, p.y) - @as(i64, p.x) * @as(i64, q.y); + j = i; + } + return area; +} + +/// Even-odd ray test: is tile-space point `pt` inside `ring`? +fn ringContains(ring: []const Point, pt: Point) bool { + if (ring.len < 3) return false; + var inside = false; + const px: f64 = @floatFromInt(pt.x); + const py: f64 = @floatFromInt(pt.y); + var j: usize = ring.len - 1; + for (ring, 0..) |p, i| { + const q = ring[j]; + const ax: f64 = @floatFromInt(p.x); + const ay: f64 = @floatFromInt(p.y); + const bx: f64 = @floatFromInt(q.x); + const by: f64 = @floatFromInt(q.y); + if ((ay > py) != (by > py) and + px < (bx - ax) * (py - ay) / (by - ay) + ax) + { + inside = !inside; + } + j = i; + } + return inside; +} + +/// Orient + order a feature's clipped area rings into MVT multipolygon parts so +/// holes are SUBTRACTED instead of filled (e.g. an island inside a sea/depth +/// area). Classify each ring by geometric nesting depth (even = exterior, odd = +/// hole), force exteriors to a positive signed area (clockwise in y-down tile +/// space) and holes to negative, and emit each exterior immediately followed by +/// the holes it directly contains. This is independent of the FSPT USAG tags, and +/// keeps disjoint multi-part areas (multiple exteriors) working as a proper +/// multipolygon. `rings` are the clipped rings (open, >= 3 pts); returned parts +/// may reverse a ring into a fresh copy. The scene emitter and the ownership +/// partition both wind rings through this, so they agree. +pub fn orientAreaRings(a: std.mem.Allocator, rings: []const []const Point) ![]const []const Point { + const n = rings.len; + const depth = try a.alloc(usize, n); + for (rings, 0..) |ri, i| { + var d: usize = 0; + for (rings, 0..) |rj, j| { + if (i != j and ringContains(rj, ri[0])) d += 1; + } + depth[i] = d; + } + + const done = try a.alloc(bool, n); + @memset(done, false); + var out = std.ArrayList([]const Point).empty; + + const emit = struct { + fn one(al: std.mem.Allocator, list: *std.ArrayList([]const Point), ring: []const Point, d: usize) !void { + const want_pos = (d % 2) == 0; // even depth = exterior (positive), odd = hole + if ((ringSignedArea(ring) >= 0) == want_pos) { + try list.append(al, ring); + } else { + const rev = try al.alloc(Point, ring.len); + for (ring, 0..) |p, k| rev[ring.len - 1 - k] = p; + try list.append(al, rev); + } + } + }.one; + + // Each exterior (even depth) followed by the holes it directly contains, so a + // decoder attaches each hole to the right exterior (depth exactly +1, inside). + for (0..n) |i| { + if (done[i] or depth[i] % 2 != 0) continue; + done[i] = true; + try emit(a, &out, rings[i], depth[i]); + for (0..n) |k| { + if (done[k] or depth[k] != depth[i] + 1) continue; + if (ringContains(rings[i], rings[k][0])) { + done[k] = true; + try emit(a, &out, rings[k], depth[k]); + } + } + } + // Safety net: emit anything not placed (malformed nesting) on its own. + for (0..n) |i| { + if (done[i]) continue; + done[i] = true; + try emit(a, &out, rings[i], depth[i]); + } + return out.items; +} + +test "orientAreaRings subtracts a hole: exterior CW (+), interior CCW (-)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // A sea-area exterior square (CCW as authored) with a smaller island hole + // inside it (also CCW as authored). y is down in tile space. + const ext = [_]Point{ + .{ .x = 0, .y = 0 }, .{ .x = 0, .y = 100 }, + .{ .x = 100, .y = 100 }, .{ .x = 100, .y = 0 }, + }; + const hole = [_]Point{ + .{ .x = 40, .y = 40 }, .{ .x = 40, .y = 60 }, + .{ .x = 60, .y = 60 }, .{ .x = 60, .y = 40 }, + }; + // Pass the hole first to prove ordering is by geometry, not input order. + const rings = [_][]const Point{ hole[0..], ext[0..] }; + const out = try orientAreaRings(a, &rings); + + try std.testing.expectEqual(@as(usize, 2), out.len); + try std.testing.expect(ringSignedArea(out[0]) > 0); + try std.testing.expect(ringSignedArea(out[1]) < 0); + try std.testing.expect(@abs(ringSignedArea(out[0])) > @abs(ringSignedArea(out[1]))); +} + +test "orientAreaRings keeps disjoint parts as separate exteriors (multipolygon)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const r0 = [_]Point{ + .{ .x = 0, .y = 0 }, .{ .x = 0, .y = 10 }, .{ .x = 10, .y = 10 }, .{ .x = 10, .y = 0 }, + }; + const r1 = [_]Point{ + .{ .x = 50, .y = 50 }, .{ .x = 50, .y = 60 }, .{ .x = 60, .y = 60 }, .{ .x = 60, .y = 50 }, + }; + const rings = [_][]const Point{ r0[0..], r1[0..] }; + const out = try orientAreaRings(a, &rings); + try std.testing.expectEqual(@as(usize, 2), out.len); + try std.testing.expect(ringSignedArea(out[0]) > 0); + try std.testing.expect(ringSignedArea(out[1]) > 0); +} diff --git a/src/tiles/pmtiles.zig b/src/tiles/pmtiles.zig index 97c19f7..9b8915d 100644 --- a/src/tiles/pmtiles.zig +++ b/src/tiles/pmtiles.zig @@ -283,6 +283,11 @@ pub const Reader = struct { header: Header, root: []Entry, arena: std.heap.ArenaAllocator, + // Deserialized leaf directories by leaf offset. A compositor probes a reader once + // per (tile, pass); re-deserializing the same leaf into the arena on EVERY probe + // grew it without bound on directory-heavy archives, so each leaf is decoded once + // and reused. Arena-backed (freed wholesale by deinit). + leaves: std.AutoHashMapUnmanaged(u64, []Entry) = .empty, pub fn init(gpa: Allocator, bytes: []const u8) !Reader { const header = try Header.parse(bytes); @@ -314,10 +319,15 @@ pub const Reader = struct { const idx = findEntry(dir, tid) orelse return null; const e = dir[idx]; if (e.run_length == 0) { - // leaf directory pointer + // leaf directory pointer — decoded once per distinct leaf, then cached const a = r.arena.allocator(); - const raw = try maybeDecompress(a, r.bytes[@intCast(r.header.leaf_dir_offset + e.offset)..][0..e.length], r.header.internal_compression); - dir = try deserializeDir(a, raw); + const gop = try r.leaves.getOrPut(a, e.offset); + if (!gop.found_existing) { + errdefer _ = r.leaves.remove(e.offset); + const raw = try maybeDecompress(a, r.bytes[@intCast(r.header.leaf_dir_offset + e.offset)..][0..e.length], r.header.internal_compression); + gop.value_ptr.* = try deserializeDir(a, raw); + } + dir = gop.value_ptr.*; continue; } if (tid < e.tile_id + e.run_length) { @@ -357,7 +367,8 @@ fn findEntry(dir: []const Entry, tid: u64) ?usize { // ---- writer ------------------------------------------------------------- -pub const InputTile = struct { z: u8, x: u32, y: u32, mvt: []const u8 }; +// A whole-tile input for the batch `write` below (test-only; the live path is StreamWriter). +const InputTile = struct { z: u8, x: u32, y: u32, mvt: []const u8 }; pub const WriteOptions = struct { metadata_json: []const u8 = "{}", @@ -366,6 +377,7 @@ pub const WriteOptions = struct { max_lon_e7: i32 = 1800000000, max_lat_e7: i32 = 850000000, tile_type: TileType = .mvt, // .mlt for MapLibre Tile output + center_zoom: ?u8 = null, // header center zoom; defaults to the archive's min zoom }; /// Streaming archive writer: feed tiles one at a time (each gzipped + content- @@ -471,7 +483,12 @@ pub const StreamWriter = struct { } try merged.append(a, e); } + defer merged.deinit(a); const dirs = try buildDirectories(a, merged.items); + // `dirs` + `merged` are copied into `out` below, so free them once the archive prefix + // is assembled (they are heap-owned by `a`; `leaves` may be the empty sentinel). + defer a.free(dirs.root); + defer if (dirs.leaves.len > 0) a.free(dirs.leaves); const metadata = opts.metadata_json; const root_off: u64 = HEADER_LEN; const meta_off: u64 = root_off + dirs.root.len; @@ -500,7 +517,7 @@ pub const StreamWriter = struct { .min_lat_e7 = opts.min_lat_e7, .max_lon_e7 = opts.max_lon_e7, .max_lat_e7 = opts.max_lat_e7, - .center_zoom = min_z, + .center_zoom = opts.center_zoom orelse min_z, .center_lon_e7 = @divTrunc(opts.min_lon_e7 + opts.max_lon_e7, 2), .center_lat_e7 = @divTrunc(opts.min_lat_e7 + opts.max_lat_e7, 2), }; @@ -530,8 +547,10 @@ pub const StreamWriter = struct { } }; -/// Build a PMTiles archive from MVT tiles (gzipped + deduped). Caller owns it. -pub fn write(gpa: Allocator, tiles: []const InputTile, opts: WriteOptions) ![]u8 { +// Build a PMTiles archive from a whole set of MVT tiles in one call (gzipped + +// deduped). The live baker streams via StreamWriter instead; this stays as a +// self-contained round-trip fixture for the reader tests below. +fn write(gpa: Allocator, tiles: []const InputTile, opts: WriteOptions) ![]u8 { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const a = arena.allocator(); @@ -618,14 +637,14 @@ pub fn write(gpa: Allocator, tiles: []const InputTile, opts: WriteOptions) ![]u8 .clustered = 1, .internal_compression = .none, .tile_compression = .gzip, - .tile_type = .mvt, + .tile_type = opts.tile_type, .min_zoom = min_z, .max_zoom = max_z, .min_lon_e7 = opts.min_lon_e7, .min_lat_e7 = opts.min_lat_e7, .max_lon_e7 = opts.max_lon_e7, .max_lat_e7 = opts.max_lat_e7, - .center_zoom = min_z, + .center_zoom = opts.center_zoom orelse min_z, .center_lon_e7 = @divTrunc(opts.min_lon_e7 + opts.max_lon_e7, 2), .center_lat_e7 = @divTrunc(opts.min_lat_e7 + opts.max_lat_e7, 2), }; @@ -726,6 +745,18 @@ test "large archive shards the root into leaf directories and still round-trips" } // An absent tile in the sharded archive still resolves to null. try std.testing.expect((try r.getTile(gpa, 15, 1, 1)) == null); + + // Repeated probes reuse the cached leaf decodes — the arena must not grow once + // every touched leaf is resident. (A compositor probes a reader once per + // (tile, pass); re-deserializing a leaf per probe was unbounded growth.) + const probes = [_]u32{ 0, 4095, 4096, 12345, N - 1 }; + for (probes) |i| _ = try r.getCompressed(15, i, 0); + const cap_before = r.arena.queryCapacity(); + for (0..100) |_| { + for (probes) |i| _ = try r.getCompressed(15, i, 0); + _ = try r.getCompressed(15, 1, 1); // absent probes walk the directory too + } + try std.testing.expectEqual(cap_before, r.arena.queryCapacity()); } test "header serialize/parse round-trip" { diff --git a/src/tiles/tile.zig b/src/tiles/tile.zig index 3210165..b08f360 100644 --- a/src/tiles/tile.zig +++ b/src/tiles/tile.zig @@ -85,6 +85,33 @@ pub fn tileBoundsLonLat(z: u8, tx: u32, ty: u32) [4]f64 { return .{ lon0, lat0, lon1, lat1 }; } +// ---- light sector-figure reach -------------------------------------------- +// A LIGHTS feature's constructed sector legs/arcs are FIXED display-size +// figures around the light (plus ground-length directional legs), so they can +// cross into tiles the light's cell owns no ground in. Both the baker's tile +// addressing (bake_enc buildTileMap) and the runtime compositor's reach ring +// (compose.tile) widen by the same bound so the figures survive there. + +const EARTH_CIRCUM_M: f64 = 40075016.686; + +/// Worst-case reach of a light's display-mm sector figures as a fraction of a +/// tile — ~constant at every zoom (offset_tiles = mm * PX_PER_MM / 512, the +/// 512-CSS-px tile the figures are sized against; S-52 legs ~25 mm / arcs +/// ~20 mm ≈ 0.2 tile; 1.0 is generous headroom). +pub const LIGHT_AUG_REACH_TILES: f64 = 1.0; + +/// Worst-case tile-unit reach of sector figures at zoom z, latitude `lat`: +/// display-mm figures reach a ~constant LIGHT_AUG_REACH_TILES, ground-length +/// legs (directional lights) reach range_m metres = range_m·2^z/(cosφ·C) tiles. +/// Never below the mm bound — a cell with figures always reaches at least the +/// display-sized ones. +pub fn lightReachTiles(range_m: f64, z: u8, lat: f64) f64 { + if (range_m <= 0) return LIGHT_AUG_REACH_TILES; + const cos_lat = @max(@cos(lat * std.math.pi / 180.0), 1e-6); + const scale: f64 = @floatFromInt(@as(u64, 1) << @intCast(z)); + return @max(LIGHT_AUG_REACH_TILES, range_m * scale / (cos_lat * EARTH_CIRCUM_M)); +} + /// Inclusive tile box used for clipping (extent +/- buffer). pub const Box = struct { min: i32, @@ -211,6 +238,18 @@ pub fn clipLine(a: Allocator, line: []const mvt.Point, b: Box) ![][]mvt.Point { return parts.items; } +/// Clip a projected line to a tile box and simplify each surviving run (dropping +/// runs shorter than 2 points). The common "clip a line for this tile" composite. +pub fn clipSimplifyLine(a: Allocator, proj: []const mvt.Point, box: Box) ![]const []const mvt.Point { + const sub = try clipLine(a, proj, box); + var out = std.ArrayList([]const mvt.Point).empty; + for (sub) |run| { + const s = try simplifyRing(a, run); + if (s.len >= 2) try out.append(a, s); + } + return out.items; +} + fn eqPt(a: mvt.Point, b: mvt.Point) bool { return a.x == b.x and a.y == b.y; } diff --git a/src/tiles/tiles.zig b/src/tiles/tiles.zig index c8202d4..069c9bc 100644 --- a/src/tiles/tiles.zig +++ b/src/tiles/tiles.zig @@ -6,6 +6,8 @@ //! pmtiles — PMTiles v3 archive read/write //! tile — web-mercator tile math: projection, extent, clipping, //! simplification (the geometry side of tiling) +//! band — compilation-scale -> zoom-range mapping (navigational bands) +//! filemap — cross-platform read-only mmap of a file (std has no portable one) //! //! Pure std; the leaf bundle everything tile-shaped builds on. @@ -14,6 +16,8 @@ pub const mlt = @import("mlt.zig"); pub const gzip = @import("gzip.zig"); pub const pmtiles = @import("pmtiles.zig"); pub const tile = @import("tile.zig"); +pub const band = @import("band.zig"); +pub const filemap = @import("filemap.zig"); test { _ = mvt; @@ -21,4 +25,6 @@ test { _ = gzip; _ = pmtiles; _ = tile; + _ = band; + _ = filemap; } diff --git a/tools/ascii.zig b/tools/ascii.zig index 50b8aa1..8fbde40 100644 --- a/tools/ascii.zig +++ b/tools/ascii.zig @@ -90,7 +90,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { } else chart.Chart.openPath(path, rules, false) catch return usageErr("cannot open source"); defer c.deinit(); - var m = render.resolve.MarinerSettings{ .display_other = true }; + var m = render.resolve.Settings{ .display_other = true }; m.scheme = switch (palette) { .day => .day, .dusk => .dusk, @@ -121,7 +121,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { // ctrl-c) quits. cbreak-style input (no echo/canonical, OPOST kept so \n still // carriage-returns), alternate screen + hidden cursor, terminal re-measured // every frame so a resize just repaints. -fn runAsciiTui(io: std.Io, a: std.mem.Allocator, c: *chart.Chart, lon0: f64, lat0: f64, zoom0: f64, palette: render.resolve.PaletteId, m: *render.resolve.MarinerSettings, ansi: bool, kitty: bool) !void { +fn runAsciiTui(io: std.Io, a: std.mem.Allocator, c: *chart.Chart, lon0: f64, lat0: f64, zoom0: f64, palette: render.resolve.PaletteId, m: *render.resolve.Settings, ansi: bool, kitty: bool) !void { // The interactive TUI is POSIX-only: std.posix.termios is `void` on Windows, // so gate the whole raw-mode body out at comptime (same idiom as common.zig's // terminalSize). The non-interactive `ascii` render paths stay cross-platform. @@ -274,7 +274,7 @@ fn runAsciiTui(io: std.Io, a: std.mem.Allocator, c: *chart.Chart, lon0: f64, lat // 2. S-101 portrayal — the ';'-separated Key:Value instruction stream the Lua // rules emit (portray.portrayCell), RAW and PARSED into // symbols / lines / fills / texts / aug figures -// (s101_instr.parse). +// (s101.instructions.parse). // 3. Resolved calls — what the portrayal BECOMES after geometry resolution: // the Surface vtable calls, captured by the recording // InspectSurface (render/inspect.zig) driven through diff --git a/tools/bake.zig b/tools/bake.zig index 7c89c94..f6a47b1 100644 --- a/tools/bake.zig +++ b/tools/bake.zig @@ -1,60 +1,38 @@ +//! `bake -o [--rules DIR] [--from-archives]` — produce a +//! LIVE-composite structure on disk. Bake each chart to its OWN native-scale PMTiles under +//! `/tiles/` (with its M_COVR coverage embedded in the metadata), then open a resident +//! compositor over them and write the ownership partition to `/partition.tpart`. There is +//! NO merged archive: a runtime compositor (`ComposeSource` / the `compose-tile` command / the C +//! ABI `tile57_compose_*`) serves any tile ON DEMAND from this structure, so the per-chart bakes stay +//! dumb + cacheable and the partition holds all cross-cell ownership. Native scale only — deeper +//! coarse zooms are left to the client camera + MapLibre overzoom. +//! +//! `--from-archives`: `` is ALREADY a directory of per-chart archives (*.pmtiles / *.cell.tmp); +//! skip the bake and only (re)build the partition sidecar into `/partition.tpart` over them +//! — the fast re-partition loop over a tiles dir. + const std = @import("std"); -const engine = @import("engine"); -const assets = @import("assets"); -const bundle = @import("bundle"); // chart-bundle pipeline (asset emitters etc.) — the lib owns it +const chart = @import("chart"); // per-chart bake (bakeChartBytes) + freeBytes +const compose = @import("compose"); // openComposeSourceFiles + serializePartition (the resident compositor) const common = @import("common.zig"); const Flags = common.Flags; const usageErr = common.usageErr; const resolveRulesDir = common.resolveRulesDir; -const resolveCatalogDir = common.resolveCatalogDir; -const VERSION = common.VERSION; -const DEFAULT_MINZOOM = common.DEFAULT_MINZOOM; -const DEFAULT_MAXZOOM = common.DEFAULT_MAXZOOM; -const DEFAULT_LRU_BUDGET = common.DEFAULT_LRU_BUDGET; -const DEFAULT_SUPER_DZ = common.DEFAULT_SUPER_DZ; -/// `bake -o [--rules DIR] [--catalog DIR] -/// [--minzoom N] [--maxzoom N] [--lru N] [--superdz N] [--created ISO8601]` — -/// THE bake command. A single cell or a whole ENC_ROOT, streamed through the same -/// lazy banded bake into a self-contained chart bundle: tiles/chart.pmtiles + -/// assets/colortables.json + sprite-mln + style-{day,dusk,night}.json + -/// manifest.json (pins schema_version, couples tiles to portrayal). pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { var base: ?[]const u8 = null; var out: ?[]const u8 = null; var rules: ?[]const u8 = null; - var catalog: ?[]const u8 = null; - var created: []const u8 = ""; - var minzoom: u8 = DEFAULT_MINZOOM; - var maxzoom: u8 = DEFAULT_MAXZOOM; - var lru: usize = DEFAULT_LRU_BUDGET; // lazy-bake tuning: parsed cells held resident - var super_dz: u8 = DEFAULT_SUPER_DZ; // lazy-bake tuning: spatial super-tile depth - var format: engine.scene.TileFormat = .mlt; // tile encoding: mlt (default) or mvt - var existing: []const u8 = ""; // cross-pack best-available: a peer pack's ENC_ROOT + var from_archives = false; // is a dir of pre-baked archives — skip baking, only build the partition var f = Flags{ .args = args }; while (f.next()) |arg| { if (std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "--output")) { out = f.val(arg) orelse return; - } else if (std.mem.eql(u8, arg, "--existing")) { - existing = f.val(arg) orelse return; } else if (std.mem.eql(u8, arg, "--rules")) { rules = f.val(arg) orelse return; - } else if (std.mem.eql(u8, arg, "--catalog")) { - catalog = f.val(arg) orelse return; - } else if (std.mem.eql(u8, arg, "--created")) { - created = f.val(arg) orelse return; - } else if (std.mem.eql(u8, arg, "--minzoom")) { - minzoom = f.int(u8, arg) orelse return; - } else if (std.mem.eql(u8, arg, "--maxzoom")) { - maxzoom = f.int(u8, arg) orelse return; - } else if (std.mem.eql(u8, arg, "--lru")) { - lru = f.int(usize, arg) orelse return; - } else if (std.mem.eql(u8, arg, "--superdz")) { - super_dz = f.int(u8, arg) orelse return; - } else if (std.mem.eql(u8, arg, "--format")) { - const v = f.val(arg) orelse return; - format = if (std.mem.eql(u8, v, "mlt")) .mlt else if (std.mem.eql(u8, v, "mvt")) .mvt else return usageErr("--format must be mvt or mlt"); + } else if (std.mem.eql(u8, arg, "--from-archives")) { + from_archives = true; } else if (std.mem.startsWith(u8, arg, "-")) { return usageErr("unknown flag"); } else if (base == null) { @@ -63,36 +41,95 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { return usageErr("unexpected argument (cell updates are auto-discovered next to the .000)"); } } - const base_path = base orelse return usageErr("missing input"); const out_dir = out orelse return usageErr("missing -o/--output "); - if (minzoom > maxzoom) return usageErr("--minzoom must be <= --maxzoom"); - if (maxzoom > 24) return usageErr("--maxzoom too large (max 24)"); - if (lru < 1) return usageErr("--lru must be >= 1"); + const rules_dir = resolveRulesDir(rules); + + // The per-chart archive paths that back the compositor. + var archive_paths = std.ArrayList([]const u8).empty; + + if (from_archives) { + // is a directory of pre-baked *.pmtiles / *.cell.tmp — read them in place. + var dir = std.Io.Dir.cwd().openDir(io, base_path, .{ .iterate = true }) catch return usageErr("cannot open archives dir"); + defer dir.close(io); + var walker = dir.walk(a) catch return usageErr("cannot walk archives dir"); + defer walker.deinit(); + while (walker.next(io) catch null) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".cell.tmp") and !std.mem.endsWith(u8, entry.path, ".pmtiles")) continue; + archive_paths.append(a, std.fs.path.join(a, &.{ base_path, entry.path }) catch continue) catch {}; + } + if (archive_paths.items.len == 0) return usageErr("no *.pmtiles / *.cell.tmp archives found"); + std.debug.print("re-partition: {d} pre-baked archives from {s}\n", .{ archive_paths.items.len, base_path }); + } else { + // Bake each chart (dedup by stem — a boundary chart shared by two districts bakes once) + // to its own /tiles/.pmtiles. + const tiles_dir = try std.fs.path.join(a, &.{ out_dir, "tiles" }); + try std.Io.Dir.cwd().createDirPath(io, tiles_dir); + + var cell_paths = std.ArrayList([]const u8).empty; + if (std.mem.endsWith(u8, base_path, ".000")) { + try cell_paths.append(a, base_path); + } else { + var dir = std.Io.Dir.cwd().openDir(io, base_path, .{ .iterate = true }) catch return usageErr("cannot open ENC_ROOT"); + defer dir.close(io); + var seen = std.StringHashMap(void).init(a); + var walker = dir.walk(a) catch return usageErr("cannot walk ENC_ROOT"); + defer walker.deinit(); + while (walker.next(io) catch null) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".000")) continue; + const stem = std.fs.path.stem(std.fs.path.basename(entry.path)); + if (seen.contains(stem)) continue; + seen.put(a.dupe(u8, stem) catch continue, {}) catch {}; + cell_paths.append(a, std.fs.path.join(a, &.{ base_path, entry.path }) catch continue) catch {}; + } + } + if (cell_paths.items.len == 0) return usageErr("no .000 cells found"); + + for (cell_paths.items) |cp| { + const arc = (chart.bakeChartBytes(cp, rules_dir) catch |err| { + std.debug.print(" warn: bake of {s} failed ({s}); skipping\n", .{ cp, @errorName(err) }); + continue; + }) orelse continue; // no M_COVR coverage — not a composable cell + defer chart.freeBytes(arc); + const stem = std.fs.path.stem(std.fs.path.basename(cp)); + const name = std.fmt.allocPrint(a, "{s}.pmtiles", .{stem}) catch continue; + const arc_path = std.fs.path.join(a, &.{ tiles_dir, name }) catch continue; + std.Io.Dir.cwd().writeFile(io, .{ .sub_path = arc_path, .data = arc }) catch |err| { + std.debug.print(" warn: could not write {s} ({s})\n", .{ arc_path, @errorName(err) }); + continue; + }; + archive_paths.append(a, arc_path) catch {}; + if (archive_paths.items.len % 25 == 0) std.debug.print(" baked {d}/{d} cells…\n", .{ archive_paths.items.len, cell_paths.items.len }); + } + if (archive_paths.items.len == 0) return usageErr("no cells baked (no .000 with M_COVR found)"); + } + + // Sort the archive paths so the ownership tie-break (which falls back to input order for + // archives carrying identical (date, name) keys) and the partition it produces are deterministic. + std.mem.sort([]const u8, archive_paths.items, {}, struct { + fn lt(_: void, x: []const u8, y: []const u8) bool { + return std.mem.lessThan(u8, x, y); + } + }.lt); + + // Open the resident compositor over the per-chart archives (mmap'd) and serialize its ownership + // partition to /partition.tpart — the sidecar a runtime open loads to skip the build. + const src = (compose.ComposeSource.openFiles(io, a, archive_paths.items, null) catch |err| { + std.debug.print("error: open compose source failed ({s})\n", .{@errorName(err)}); + return; + }) orelse return usageErr("no coverage-carrying archives (nothing to compose)"); + defer src.deinit(); - // The whole tiles + assets + manifest pipeline lives in the `bundle` lib module - // (bundle.bakeBundle) so any consumer (the C ABI, a Go/JS binding) emits the same - // package; the CLI just resolves args -> options and prints the summary. - const res = bundle.bakeBundle(io, a, .{ - .input = base_path, - .out_dir = out_dir, - .rules_dir = resolveRulesDir(rules), - .catalog_dir = resolveCatalogDir(catalog), - .generator = VERSION, - .created = created, - .minzoom = minzoom, - .maxzoom = maxzoom, - .lru = lru, - .super_dz = super_dz, - .format = format, - .existing_input = existing, - }) catch |err| { - std.debug.print("error: cannot bake {s} ({s})\n", .{ base_path, @errorName(err) }); + const part_bytes = src.serializePartition(a) catch |err| { + std.debug.print("error: partition serialization failed ({s})\n", .{@errorName(err)}); return; }; + const part_path = try std.fs.path.join(a, &.{ out_dir, "partition.tpart" }); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = part_path, .data = part_bytes }); std.debug.print( - "bundled {d} cell(s) -> {s}/\n tiles/chart.pmtiles + assets/colortables.json + sprite-mln + style-{{day,dusk,night}}.json + manifest.json (schema {s})\n", - .{ res.cell_count, out_dir, assets.SCHEMA_VERSION }, + "live structure -> {s}/\n {d} per-chart archive(s){s} + partition.tpart (serve z {d}..{d})\n", + .{ out_dir, src.readers.len, if (from_archives) " (in place)" else " under tiles/", src.minz, src.loop_max }, ); } diff --git a/tools/cells.zig b/tools/cells.zig index 7e1c0b7..85570c2 100644 --- a/tools/cells.zig +++ b/tools/cells.zig @@ -15,7 +15,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { return; }; defer c.deinit(); - const json = (c.cellsJson() catch null) orelse { + const json = (c.chartsJson() catch null) orelse { std.debug.print("no cells\n", .{}); return; }; diff --git a/tools/common.zig b/tools/common.zig index f7fb0d3..0ef054b 100644 --- a/tools/common.zig +++ b/tools/common.zig @@ -6,16 +6,7 @@ const std = @import("std"); const bundle = @import("bundle"); // chart-bundle pipeline (asset emitters etc.) — the lib owns it -pub const VERSION = "tile57 0.1.0"; - -pub const DEFAULT_MINZOOM: u8 = 8; -pub const DEFAULT_MAXZOOM: u8 = 16; - -// Lazy-baker tuning (bake-root): LRU budget = parsed cells kept loaded across -// super-tiles; super-tile depth = how far below a band's min zoom the spatial -// batch tile sits. Overridable via --lru / --superdz for tuning. -pub const DEFAULT_LRU_BUDGET: usize = 256; -pub const DEFAULT_SUPER_DZ: u8 = 3; +pub const VERSION = "tile57 0.3.0"; // Env access lives in the Lua C shim (Zig 0.16 gates env behind std.Io); // returns the S-101 rules dir from TILE57_S101_RULES or null. Mirrors capi.zig. @@ -151,30 +142,25 @@ pub fn printUsage() void { \\{s} — offline S-57 -> PMTiles baker / inspector \\ \\usage: - \\ tile57 bake -o [options] - \\ Bake a single S-57 cell OR a whole ENC_ROOT (every .000 + - \\ its auto-discovered updates, zoom-banded per cell by compilation - \\ scale) into a self-contained chart bundle, streamed to disk: - \\ /tiles/chart.pmtiles + assets/colortables.json + sprite-mln + - \\ style-{{day,dusk,night}}.json + manifest.json (pins schema_version, - \\ couples tiles to portrayal). - \\ -o, --output DIR output bundle directory (required) + \\ tile57 bake -o [--rules DIR] [--from-archives] + \\ Produce a live-composite structure: bake each chart (a single .000 + + \\ its auto-discovered updates, OR every .000 in an ENC_ROOT, at + \\ native band scale) to its own /tiles/.pmtiles with M_COVR + \\ coverage embedded, then write the ownership partition to + \\ /partition.tpart. A runtime compositor (compose-tile / the C ABI) + \\ serves any tile ON DEMAND from this structure — there is no merged archive. + \\ -o, --output DIR output directory (required) \\ --rules DIR S-101 portrayal rules directory (default: embedded) - \\ --catalog DIR PortrayalCatalog (default: parent of --rules) - \\ --minzoom N lowest zoom to bake (default {d}) - \\ --maxzoom N highest zoom to bake (default {d}) - \\ --created ISO8601 stamp the manifest (no wall clock in-process) - \\ --lru N parsed cells held resident (lazy-bake tuning; trade - \\ memory for fewer re-parses; default {d}) - \\ --superdz N spatial super-tile depth below a band's min zoom - \\ (lazy-bake tuning; default {d}) - \\ --format mlt|mvt tile encoding (default mlt = MapLibre Tile; - \\ mvt = Mapbox Vector Tile, kept for consumers - \\ without an MLT decoder) - \\ tile57 assets -o - \\ Emit just the portrayal assets (colortables.json today) for a - \\ catalogue, independent of any cell. - \\ tile57 style --scheme day -o + \\ --from-archives is already a dir of per-chart archives; skip + \\ baking, only (re)build partition.tpart over them + \\ tile57 compose-tile [--load-partition FILE] [-o out] [--bench N] + \\ Serve ONE composed tile on demand from a live-composite structure + \\ (the runtime compositor); --bench N times an NxN block around (x,y). + \\ tile57 assets [portrayal-catalog-dir] -o + \\ Emit the portrayal assets — colortables, linestyles, and the sprite + \\ + area-fill pattern atlases — independent of any chart. The + \\ catalogue defaults to the embedded copy. + \\ tile57 style [portrayal-catalog-dir] --scheme day -o \\ Emit one MapLibre style.json (colours from the catalogue, or \\ --colortables FILE). --scheme day|dusk|night; --source-tiles/ \\ --pmtiles-url pick the source; --sprite/--glyphs enable symbol/text @@ -183,7 +169,7 @@ pub fn printUsage() void { \\ tile57 png|pdf --view --size WxH -o \\ Render a tile or a view through the native S-52 pixel path: \\ PNG raster or deterministic vector PDF (real text objects). - \\ Sources: an S-57 cell (single-cell portrayal) or a baked .pmtiles + \\ Sources: an S-57 chart (single-chart portrayal) or a baked .pmtiles \\ bundle (tile replay). --dq data-quality overlay; --scale F \\ physical-size multiplier; --palette day|dusk|night. \\ tile57 ascii --view [--size COLSxROWS (default: terminal size)] [--ansi] [--kitty] @@ -192,21 +178,21 @@ pub fn printUsage() void { \\ tile57 explore [--class ACR[,ACR..]] [--object FOID|RCID|INDEX] \\ Dump, per feature, the RAW S-57 (class + attributes), the S-101 \\ portrayal instruction stream (raw + parsed), and the resolved - \\ Surface draw calls. Takes a SINGLE .000 cell (auto-applying its + \\ Surface draw calls. Takes a SINGLE .000 chart (auto-applying its \\ .001+ updates), or an ENC_ROOT with --view LON,LAT,ZOOM (or a - \\ "…/#v=LON,LAT,ZOOM" share URL) to pull just the cells under that + \\ "…/#v=LON,LAT,ZOOM" share URL) to pull just the charts under that \\ viewport (--viewport WxH overrides the assumed 1280x800 screen). \\ --zoom N picks the resolving tile; --json; --no-resolve skips the draw-call pass; \\ --tui opens the two-pane explorer (arrows select, / filters, q \\ quits); --kitty adds, in console mode, an isolated thumbnail of - \\ each feature's resolved render, and in the TUI a LIVE CELL MAP that - \\ frames the selection (whole cell on a class header, zoomed in to + \\ each feature's resolved render, and in the TUI a LIVE CHART MAP that + \\ frames the selection (whole chart on a class header, zoomed in to \\ frame a feature; m toggles map-only) — for graphics terminals. \\ tile57 inspect [z x y] \\ tile57 cell - \\ tile57 objlcount [prim] (corpus scan: find cells with an object class) + \\ tile57 objlcount [prim] (corpus scan: find charts with an object class) \\ tile57 version \\ tile57 help \\ - , .{ VERSION, DEFAULT_MINZOOM, DEFAULT_MAXZOOM, DEFAULT_LRU_BUDGET, DEFAULT_SUPER_DZ }); + , .{VERSION}); } diff --git a/tools/compose_tile.zig b/tools/compose_tile.zig new file mode 100644 index 0000000..e9fabad --- /dev/null +++ b/tools/compose_tile.zig @@ -0,0 +1,201 @@ +//! `compose-tile [--load-partition FILE] [-o out] [--bench N]` — serve one +//! composed tile ON DEMAND from a resident ownership partition + mmap'd per-cell archives (the +//! runtime-compositor path), byte-identical to the batch. `--bench N` then serves an N×N tile block +//! around (x,y) to report per-tile serving latency, amortising the one-time open. +//! +//! `compose-tile --scan Z0[..Z1] [--load-partition FILE]` — compose EVERY tile in the +//! source bounds at those zooms and report each tile whose clip left an open ring walk (the +//! geometry boolean's wedge-artifact diagnostic): on correct booleans the count is always zero, so +//! any hit names a broken tile exactly. The artifact sweep to run after touching the boolean. + +const std = @import("std"); +const engine = @import("engine"); +const compose = @import("compose"); +const geometry = @import("geometry"); +const common = @import("common.zig"); +const Flags = common.Flags; +const usageErr = common.usageErr; + +// Monotonic nanoseconds (std.time has no Timer in this toolchain). +fn nowNs() u64 { + var ts: std.os.linux.timespec = undefined; + _ = std.os.linux.clock_gettime(std.os.linux.CLOCK.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) * std.time.ns_per_s + @as(u64, @intCast(ts.nsec)); +} + +pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { + var dir_path: ?[]const u8 = null; + var zs: ?[]const u8 = null; + var xs: ?[]const u8 = null; + var ys: ?[]const u8 = null; + var out: ?[]const u8 = null; + var load_partition: ?[]const u8 = null; + var bench: u32 = 0; + var scan: ?[]const u8 = null; + + var f = Flags{ .args = args }; + while (f.next()) |arg| { + if (std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "--output")) { + out = f.val(arg) orelse return; + } else if (std.mem.eql(u8, arg, "--load-partition")) { + load_partition = f.val(arg) orelse return; + } else if (std.mem.eql(u8, arg, "--bench")) { + bench = f.int(u32, arg) orelse return; + } else if (std.mem.eql(u8, arg, "--scan")) { + scan = f.val(arg) orelse return; + } else if (std.mem.startsWith(u8, arg, "-")) { + return usageErr("unknown flag"); + } else if (dir_path == null) { + dir_path = arg; + } else if (zs == null) { + zs = arg; + } else if (xs == null) { + xs = arg; + } else if (ys == null) { + ys = arg; + } else return usageErr("unexpected argument"); + } + const dir = dir_path orelse return usageErr("missing "); + // --scan Z0[..Z1] needs no positional tile address. + var scan_z0: u8 = 0; + var scan_z1: u8 = 0; + if (scan) |sv| { + if (std.mem.indexOf(u8, sv, "..")) |dots| { + scan_z0 = std.fmt.parseInt(u8, sv[0..dots], 10) catch return usageErr("bad --scan zoom"); + scan_z1 = std.fmt.parseInt(u8, sv[dots + 2 ..], 10) catch return usageErr("bad --scan zoom"); + } else { + scan_z0 = std.fmt.parseInt(u8, sv, 10) catch return usageErr("bad --scan zoom"); + scan_z1 = scan_z0; + } + if (scan_z1 < scan_z0) return usageErr("bad --scan zoom range"); + } + const z = if (scan != null) 0 else std.fmt.parseInt(u8, zs orelse return usageErr("missing z"), 10) catch return usageErr("bad z"); + const tx = if (scan != null) 0 else std.fmt.parseInt(u32, xs orelse return usageErr("missing x"), 10) catch return usageErr("bad x"); + const ty = if (scan != null) 0 else std.fmt.parseInt(u32, ys orelse return usageErr("missing y"), 10) catch return usageErr("bad y"); + + // Enumerate the per-cell archives (sorted, like `compose --from-archives`). + var paths = std.ArrayList([]const u8).empty; + { + var d = std.Io.Dir.cwd().openDir(io, dir, .{ .iterate = true }) catch return usageErr("cannot open archive dir"); + defer d.close(io); + var walker = d.walk(a) catch return usageErr("cannot walk archive dir"); + defer walker.deinit(); + while (walker.next(io) catch null) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".cell.tmp") and !std.mem.endsWith(u8, entry.path, ".pmtiles")) continue; + paths.append(a, std.fs.path.join(a, &.{ dir, entry.path }) catch continue) catch {}; + } + } + std.mem.sort([]const u8, paths.items, {}, struct { + fn lt(_: void, x: []const u8, y: []const u8) bool { + return std.mem.lessThan(u8, x, y); + } + }.lt); + if (paths.items.len == 0) return usageErr("no *.cell.tmp / *.pmtiles archives found"); + + // Read the partition sidecar, if provided (a missing/stale one just rebuilds inside open). + var load_bytes: ?[]const u8 = null; + if (load_partition) |lp| load_from: { + var lf = std.Io.Dir.cwd().openFile(io, lp, .{}) catch break :load_from; + defer lf.close(io); + const st = lf.stat(io) catch break :load_from; + const n: usize = @intCast(st.size); + if (n == 0) break :load_from; + const buf = a.alloc(u8, n) catch break :load_from; + _ = lf.readPositionalAll(io, buf, 0) catch break :load_from; + load_bytes = buf; + } + + // Open the resident source (mmap archives + partition once) — the amortised cost. + const open_t0 = nowNs(); + const src = (compose.ComposeSource.openFiles(io, a, paths.items, load_bytes) catch |err| { + std.debug.print("error: open compose source failed ({s})\n", .{@errorName(err)}); + return; + }) orelse { + std.debug.print("no coverage-carrying archives in {s}\n", .{dir}); + return; + }; + defer src.deinit(); + const open_ms = @as(f64, @floatFromInt(nowNs() - open_t0)) / 1e6; + std.debug.print("opened {d} cell(s), partition {s}, in {d:.1} ms (serve z {d}..{d})\n", .{ src.readers.len, if (load_bytes != null) "loaded" else "built", open_ms, src.minz, src.loop_max }); + + // Artifact sweep: compose every in-bounds tile at the scan zooms and report + // each one whose clip dead-ended a ring walk (open chain ⇒ chord artifact). + if (scan != null) { + var arena_state = std.heap.ArenaAllocator.init(a); + defer arena_state.deinit(); + var bad_tiles: usize = 0; + var total: usize = 0; + var sz = scan_z0; + while (sz <= scan_z1) : (sz += 1) { + const scale: f64 = @floatFromInt(@as(u32, 1) << @intCast(sz)); + const tl = engine.tile.lonLatToWorld(src.bounds[0], src.bounds[3]); + const br = engine.tile.lonLatToWorld(src.bounds[2], src.bounds[1]); + const x0 = compose.worldAxisToTile(tl[0], scale); + const x1 = compose.worldAxisToTile(br[0], scale); + const y0 = compose.worldAxisToTile(tl[1], scale); + const y1 = compose.worldAxisToTile(br[1], scale); + var zx = x0; + while (zx <= x1) : (zx += 1) { + var zy = y0; + while (zy <= y1) : (zy += 1) { + const before = geometry.boolean.open_chain_walks; + const big_before = geometry.boolean.large_open_chains; + const aa = arena_state.allocator(); + const r = src.tile(aa, sz, zx, zy) catch |err| { + std.debug.print("z{d}/{d}/{d}: ERROR {s}\n", .{ sz, zx, zy, @errorName(err) }); + continue; + }; + if (r.tile != null) total += 1; + const chains = geometry.boolean.open_chain_walks - before; + const big = geometry.boolean.large_open_chains - big_before; + if (chains > 0) { + bad_tiles += 1; + std.debug.print("z{d}/{d}/{d}: {d} open chain(s), {d} large\n", .{ sz, zx, zy, chains, big }); + } + _ = arena_state.reset(.retain_capacity); + } + } + std.debug.print("scan z{d} done ({d} composed so far, {d} bad)\n", .{ sz, total, bad_tiles }); + } + std.debug.print("scan complete: {d} tiles composed, {d} with open chains\n", .{ total, bad_tiles }); + return; + } + + // Serve the requested tile. + const serve_t0 = nowNs(); + const res = try src.tile(a, z, tx, ty); + const tile = res.tile; + const serve_ms = @as(f64, @floatFromInt(nowNs() - serve_t0)) / 1e6; + if (tile) |t| { + std.debug.print("served z{d}/{d}/{d}: {d} bytes (raw MLT, owned={}) in {d:.3} ms\n", .{ z, tx, ty, t.len, res.owned, serve_ms }); + if (out) |op| std.Io.Dir.cwd().writeFile(io, .{ .sub_path = op, .data = t }) catch |err| + std.debug.print(" warn: could not write {s} ({s})\n", .{ op, @errorName(err) }); + a.free(t); + } else std.debug.print("z{d}/{d}/{d}: no cell owns this tile (null) in {d:.3} ms\n", .{ z, tx, ty, serve_ms }); + + // Optional benchmark: serve an N×N block around (tx,ty), reporting amortised per-tile latency. + if (bench > 0) { + const half = bench / 2; + var served: usize = 0; + var bytes: usize = 0; + const bench_t0 = nowNs(); + var dx: u32 = 0; + while (dx < bench) : (dx += 1) { + var dy: u32 = 0; + while (dy < bench) : (dy += 1) { + const bx = (tx + dx) -| half; + const by = (ty + dy) -| half; + const tl = (src.tile(a, z, bx, by) catch continue).tile; + if (tl) |t| { + served += 1; + bytes += t.len; + a.free(t); + } + } + } + const total_ms = @as(f64, @floatFromInt(nowNs() - bench_t0)) / 1e6; + const n: f64 = @floatFromInt(bench * bench); + std.debug.print("bench: {d}/{d} tiles owned, queried {d} in {d:.1} ms = {d:.3} ms/tile ({d} bytes total)\n", .{ served, @as(u32, bench * bench), @as(u32, bench * bench), total_ms, total_ms / n, bytes }); + } +} diff --git a/tools/explore.zig b/tools/explore.zig index efbd621..23d2150 100644 --- a/tools/explore.zig +++ b/tools/explore.zig @@ -95,7 +95,7 @@ const ExRow = struct { s101: []const u8, // S-101 feature-class name ("Light") or "" attrs: []const ExAttr, raw: ?[]const u8, // level 2: raw instruction stream - parsed: ?engine.s101_instr.Portrayal, // level 2: parsed + parsed: ?engine.s101_instructions.Portrayal, // level 2: parsed resolved: ?ExLevel3, // level 3 thumb: ?ExThumb, // --kitty: where to render this feature's thumbnail (null = no geometry) }; @@ -422,7 +422,7 @@ fn exBuildRow(a: std.mem.Allocator, cell: *engine.s57.Cell, cell_name: []const u } const raw: ?[]const u8 = if (portrayal) |p| (if (fi < p.len) p[fi] else null) else null; - const parsed: ?engine.s101_instr.Portrayal = if (raw) |s| (engine.s101_instr.parse(a, s) catch null) else null; + const parsed: ?engine.s101_instructions.Portrayal = if (raw) |s| (engine.s101_instructions.parse(a, s) catch null) else null; const resolved: ?ExLevel3 = if (ctx) |c| try exFoldResolved(a, f, class, c) else null; const thumb: ?ExThumb = if (F.kitty) exThumbView(a, cell, f) else null; @@ -709,7 +709,7 @@ fn exStreamCell( out: *OutBuf, first: *bool, palette: render.resolve.PaletteId, - m: *const render.resolve.MarinerSettings, + m: *const render.resolve.Settings, ) !void { const ca = ca_arena.allocator(); const portrayal: ?[]const ?[]const u8 = engine.portray.portrayCell(ca, cell, rules) catch null; @@ -784,7 +784,7 @@ fn viewportBbox(lon: f64, lat: f64, zoom: f64, w_px: f64, h_px: f64) [4]f64 { pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { if (args.len < 3) { - std.debug.print("usage: tile57 explore [--class ACR[,ACR..]] [--object FOID|RCID|INDEX] [--zoom N] [--view LON,LAT,ZOOM|URL] [--viewport WxH] [--json] [--tui] [--kitty] [--no-resolve] [--rules DIR]\n", .{}); + std.debug.print("usage: tile57 explore [--class ACR[,ACR..]] [--object FOID|RCID|INDEX] [--zoom N] [--view LON,LAT,ZOOM|URL] [--viewport WxH] [--json] [--tui] [--kitty] [--no-resolve] [--meta] [--rules DIR]\n", .{}); return; } const path = args[2]; @@ -792,6 +792,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { var json = false; var tui = false; var kitty = false; + var meta_bounds = false; var rules_flag: ?[]const u8 = null; var view: ?View = null; var viewport_w: f64 = 1280; // the screen the viewport filter assumes (CSS px) @@ -823,6 +824,8 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { F.kitty = true; } else if (std.mem.eql(u8, arg, "--no-resolve")) { F.do_resolve = false; + } else if (std.mem.eql(u8, arg, "--meta")) { + meta_bounds = true; // resolve meta-object boundaries (M_NSYS/M_COVR/…) the inspection view shows } else if (std.mem.eql(u8, arg, "--rules")) { rules_flag = f.val("--rules") orelse return; } else return usageErr("unknown flag"); @@ -838,8 +841,9 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { // (chart.renderFeature); the --tui LIVE CELL MAP instead frames the selection // over the real chart (chart.renderCellView — see exTuiMap). const palette: render.resolve.PaletteId = .day; - var m = render.resolve.MarinerSettings{ .display_other = true }; + var m = render.resolve.Settings{ .display_other = true }; m.scheme = .day; + m.show_meta_bounds = meta_bounds; // explore inspects one or more source cells. `dir` stays open for the whole run // (the TUI re-reads cells lazily to rebuild level 3 + the map render). @@ -994,7 +998,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { // ISOLATED — only this feature's portrayal (chart.renderFeature, only_fi = fi) // on a solid background, NOT a map crop of the surrounding scene. Any failure // prints a short note instead of an image (graceful degradation), never an error. -fn exAppendThumb(a: std.mem.Allocator, out: *std.ArrayList(u8), cell: *engine.s57.Cell, portrayal: ?[]const ?[]const u8, fi: usize, row: ExRow, palette: render.resolve.PaletteId, m: *const render.resolve.MarinerSettings) !void { +fn exAppendThumb(a: std.mem.Allocator, out: *std.ArrayList(u8), cell: *engine.s57.Cell, portrayal: ?[]const ?[]const u8, fi: usize, row: ExRow, palette: render.resolve.PaletteId, m: *const render.resolve.Settings) !void { const tv = row.thumb orelse { try out.appendSlice(a, " resolved render: (no renderable geometry)\n"); return; @@ -1393,7 +1397,7 @@ fn exLoadCell( // + alt-screen scaffolding as `tile57 ascii --tui`; dependency-free. The map is // transmit-once-per-view + place, deleted each frame — the same cached-region // pattern as the ascii kitty TUI, so it never scrolls the layout. -fn exploreTui(io: std.Io, a: std.mem.Allocator, rows: []const ExIndexRow, cells: []const ExCellSrc, dir: std.Io.Dir, rules: []const u8, F: ExFilters, kitty: bool, palette: render.resolve.PaletteId, m: *const render.resolve.MarinerSettings, source: []const u8) !void { +fn exploreTui(io: std.Io, a: std.mem.Allocator, rows: []const ExIndexRow, cells: []const ExCellSrc, dir: std.Io.Dir, rules: []const u8, F: ExFilters, kitty: bool, palette: render.resolve.PaletteId, m: *const render.resolve.Settings, source: []const u8) !void { // The interactive TUI is POSIX-only: std.posix.termios is `void` on Windows, // so gate the whole raw-mode body out at comptime (same idiom as common.zig's // terminalSize). The non-interactive `explore` paths stay cross-platform. @@ -1820,7 +1824,7 @@ fn exMapGeom(right_w: usize, left_w: usize, term_rows: usize, text_rows: usize, // cursor) is the SAME escape shape as the console `--kitty` path. The image stays // strictly within the body rows (footer clear) so its cursor-advance can't scroll // the text away. Any failure clears the image and leaves the text intact. -fn exTuiMap(io: std.Io, stdout: std.Io.File, st: *ThumbState, cell: *engine.s57.Cell, portrayal: ?[]const ?[]const u8, view: MapView, highlight: ?chart.Highlight, palette: render.resolve.PaletteId, m: *const render.resolve.MarinerSettings, geom: MapGeom) void { +fn exTuiMap(io: std.Io, stdout: std.Io.File, st: *ThumbState, cell: *engine.s57.Cell, portrayal: ?[]const ?[]const u8, view: MapView, highlight: ?chart.Highlight, palette: render.resolve.PaletteId, m: *const render.resolve.Settings, geom: MapGeom) void { const clear = struct { fn f(io_: std.Io, out: std.Io.File, s: *ThumbState) void { out.writeStreamingAll(io_, render.kitty.delete_all) catch {}; diff --git a/tools/inspect.zig b/tools/inspect.zig index a374031..fb42efe 100644 --- a/tools/inspect.zig +++ b/tools/inspect.zig @@ -21,6 +21,15 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { const y = try std.fmt.parseInt(u32, args[5], 10); if (try r.getTile(a, z, x, y)) |tile| { { + // Optional trailing `-o FILE` writes the raw (decompressed) tile + // bytes out, so tiledump and friends can chew on the same tile. + var ai: usize = 6; + while (ai + 1 < args.len) : (ai += 1) { + if (std.mem.eql(u8, args[ai], "-o")) { + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = args[ai + 1], .data = tile }); + std.debug.print(" wrote {s} ({d} bytes)\n", .{ args[ai + 1], tile.len }); + } + } // Decode with the codec matching the archive's tile type — both // return the same DecodedLayer shape, so the dump below is shared. const layers = if (h.tile_type == .mlt) diff --git a/tools/main.zig b/tools/main.zig index 2d4b9b9..a392f7b 100644 --- a/tools/main.zig +++ b/tools/main.zig @@ -1,15 +1,15 @@ //! tile57 — the offline S-57 -> PMTiles baker / inspector CLI. //! //! Subcommands: -//! bake -o [--rules DIR] [--minzoom N] [--maxzoom N] [update.001 ...] -//! Decode an S-57 base cell (applying any update files), portray it, and -//! pre-bake every web-mercator MVT tile covering the cell's bounds across -//! the requested zoom range into a clustered PMTiles archive. +//! bake -o [--rules DIR] [--from-archives] +//! Bake each chart to /tiles/.pmtiles and write the ownership +//! partition to /partition.tpart — the live-composite structure a +//! runtime compositor serves tiles from on demand. //! inspect [z x y] //! Parse a PMTiles archive (header + directory) and, if z/x/y is given, //! read+gunzip+decode that tile and list its MVT layers. //! cell -//! Decode + summarise an S-57 cell (record tally, bounds, topology). +//! Decode + summarise an S-57 chart (record tally, bounds, topology). //! version //! Print the baker version. //! help @@ -23,11 +23,13 @@ const std = @import("std"); const common = @import("common.zig"); const bake = @import("bake.zig"); +const compose_tile = @import("compose_tile.zig"); const assets = @import("assets.zig"); const sprite = @import("sprite.zig"); const pattern = @import("pattern.zig"); const sprite_mln = @import("sprite_mln.zig"); const style = @import("style.zig"); +const tiledump = @import("tiledump.zig"); const render = @import("render.zig"); const ascii = @import("ascii.zig"); const explore = @import("explore.zig"); @@ -40,6 +42,7 @@ const audit_holes = @import("audit_holes.zig"); const audit_pairs = @import("audit_pairs.zig"); const objlcount = @import("objlcount.zig"); const cell = @import("cell.zig"); +const partdbg_png = @import("partdbg_png.zig"); pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); @@ -51,6 +54,9 @@ pub fn main(init: std.process.Init) !void { if (std.mem.eql(u8, sub, "bake")) { return bake.run(io, arena, args); } + if (std.mem.eql(u8, sub, "compose-tile")) { + return compose_tile.run(io, arena, args); + } if (std.mem.eql(u8, sub, "assets")) { return assets.run(io, arena, args); @@ -72,9 +78,16 @@ pub fn main(init: std.process.Init) !void { return style.run(io, arena, args); } + if (std.mem.eql(u8, sub, "tiledump")) { + return tiledump.run(io, arena, args); + } + if (std.mem.eql(u8, sub, "png") or std.mem.eql(u8, sub, "renderpng")) { return render.run(io, arena, args, .png); } + if (std.mem.eql(u8, sub, "partdbg-png")) { + return partdbg_png.run(io, arena, args); + } if (std.mem.eql(u8, sub, "pdf")) { return render.run(io, arena, args, .pdf); diff --git a/tools/partdbg_png.zig b/tools/partdbg_png.zig new file mode 100644 index 0000000..c328e60 --- /dev/null +++ b/tools/partdbg_png.zig @@ -0,0 +1,67 @@ +//! Render one tile of a partition-debug PMTiles to a PNG — a self-check that the +//! ownership faces actually fill. Each "partition" feature is filled with its +//! `color` property using the nonzero rule (what MapLibre uses), so a blank output +//! means the geometry itself is wrong, not the viewer. +//! +//! tile57 partdbg-png + +const std = @import("std"); +const engine = @import("engine"); +const render = @import("render"); +const cv = render.canvas; + +fn hex(s: []const u8) cv.Color { + if (s.len < 7 or s[0] != '#') return .{ .r = 200, .g = 200, .b = 200 }; + const v = std.fmt.parseInt(u32, s[1..7], 16) catch return .{ .r = 200, .g = 200, .b = 200 }; + return .{ .r = @intCast((v >> 16) & 0xff), .g = @intCast((v >> 8) & 0xff), .b = @intCast(v & 0xff), .a = 210 }; +} + +pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { + if (args.len < 7) { + std.debug.print("usage: tile57 partdbg-png \n", .{}); + return; + } + const path = args[2]; + const z = try std.fmt.parseInt(u8, args[3], 10); + const x = try std.fmt.parseInt(u32, args[4], 10); + const y = try std.fmt.parseInt(u32, args[5], 10); + const out = args[6]; + + const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, a, .unlimited); + var r = try engine.pmtiles.Reader.init(a, bytes); + defer r.deinit(); + const tb = (try r.getTile(a, z, x, y)) orelse { + std.debug.print("tile {d}/{d}/{d} not found\n", .{ z, x, y }); + return; + }; + const layers = try engine.mvt.decode(a, tb); + + const W: u32 = 1024; + var rc = try render.raster.RasterCanvas.init(a, W, W); + defer rc.deinit(); + rc.clear(.{ .r = 18, .g = 22, .b = 26 }); + const canvas = rc.asCanvas(); + const sc: f32 = @as(f32, @floatFromInt(W)) / 4096.0; + + var n: usize = 0; + for (layers) |L| { + if (!std.mem.eql(u8, L.name, "partition")) continue; + for (L.features) |f| { + var col = cv.Color{ .r = 200, .g = 200, .b = 200, .a = 210 }; + for (f.properties) |pr| if (std.mem.eql(u8, pr.key, "color") and pr.value == .string) { + col = hex(pr.value.string); + }; + var rings = std.ArrayList([]const cv.Point).empty; + for (f.parts) |ring| { + const rr = try a.alloc(cv.Point, ring.len); + for (ring, 0..) |p, i| rr[i] = .{ .x = @as(f32, @floatFromInt(p.x)) * sc, .y = @as(f32, @floatFromInt(p.y)) * sc }; + try rings.append(a, rr); + } + try canvas.fillPath(rings.items, col, .nonzero); + n += 1; + } + } + const png = try render.png.encodeRgba(a, rc.px, W, W); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out, .data = png }); + std.debug.print("rendered {d} partition features from {d}/{d}/{d} -> {s}\n", .{ n, z, x, y, out }); +} diff --git a/tools/render.zig b/tools/render.zig index 8926fcf..402ad61 100644 --- a/tools/render.zig +++ b/tools/render.zig @@ -1,6 +1,6 @@ const std = @import("std"); const engine = @import("engine"); -const assets = @import("assets"); +const style = @import("style"); const sprite = @import("sprite"); const render = @import("render"); const chart = @import("chart"); @@ -19,7 +19,7 @@ const resolveRulesDir = common.resolveRulesDir; // tile (labels + declutter over the full canvas, no seams). pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: render.pixel.Output) !void { if (args.len < 4) { - std.debug.print("usage: tile57 {s} -o [--size N] [--palette day|dusk|night] [--rules DIR] [--dq] [--scale F]\n" ++ + std.debug.print("usage: tile57 {s} -o [--size N] [--palette day|dusk|night] [--rules DIR] [--dq] [--meta] [--scale F]\n" ++ " tile57 {s} --view --size WxH -o [flags]\n", .{ @tagName(output), @tagName(output) }); return; } @@ -44,7 +44,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: var size_scale: f64 = 1.0; // physical-size multiplier (S-52 mm -> true mm) var view: ?struct { lon: f64, lat: f64, zoom: f64 } = null; // Mariner settings (defaults match the app: other ON for spot soundings). - var m = render.resolve.MarinerSettings{ .display_other = true }; + var m = render.resolve.Settings{ .display_other = true }; var f = Flags{ .args = args, .i = if (tile_mode) 5 else 2 }; while (f.next()) |arg| { if (std.mem.eql(u8, arg, "-o")) { @@ -72,6 +72,8 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: rules = f.next() orelse return usageErr("--rules needs a dir"); } else if (std.mem.eql(u8, arg, "--dq")) { dq = true; // S-52 data-quality overlay (M_QUAL DQUAL* patterns) + } else if (std.mem.eql(u8, arg, "--meta")) { + m.show_meta_bounds = true; // meta-object coverage/scale/nav-system bounds inspection view } else if (std.mem.eql(u8, arg, "--scale")) { const v = f.next() orelse return usageErr("--scale needs a value"); size_scale = std.fmt.parseFloat(f64, v) catch return usageErr("bad --scale"); @@ -182,9 +184,9 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: ps.output = output; // Complex-linestyle table (idempotent; arena-backed — this run only). - const ls_srcs = try a.alloc(assets.LineStyleSrc, catalog_embed.linestyles.len); + const ls_srcs = try a.alloc(style.LineStyleSrc, catalog_embed.linestyles.len); for (catalog_embed.linestyles, 0..) |e, li| ls_srcs[li] = .{ .id = e.name, .xml = e.bytes }; - engine.scene.registerLinestylesXml(a, ls_srcs); + engine.scene.linestyle.registerLinestylesXml(a, ls_srcs); const bytes = if (from_bundle) blk: { // Bundle-sourced replay: decode each covering baked tile and re-emit diff --git a/tools/s101_coverage.zig b/tools/s101_coverage.zig index dc5ae57..bb61e4a 100644 --- a/tools/s101_coverage.zig +++ b/tools/s101_coverage.zig @@ -1,9 +1,8 @@ -//! S-57 -> S-101 portrayal attribute-coverage check -//! (Tasks 2-3 of the conformance-testability brief; see -//! specs/conformance-testability.md). Run it: `zig build s101-coverage` +//! S-57 -> S-101 portrayal attribute-coverage check. Run it: +//! `zig build s101-coverage` //! (append `-- --json out.json` / `-- --fail-on-new tools/s101_coverage_baseline.json`). -//! The gate baseline lives at tools/s101_coverage_baseline.json (tracked; /specs/ -//! is gitignored). Regenerate it after an intended change with --write-baseline. +//! The gate baseline lives at tools/s101_coverage_baseline.json (tracked). +//! Regenerate it after an intended change with --write-baseline. //! //! 1. STATIC READ-SET (Task 2): scan the vendored S-101 Portrayal Catalogue Lua //! rules and, per feature-class rule, collect every attribute the rule reads @@ -16,7 +15,7 @@ //! //! 2. ADAPTER COVERAGE (Task 3): classify each consumed attribute as //! (a) SOURCED - non-empty S-57 `alias` in catalogue.json, so the adapter's -//! resolveAttrByCode forwards it (s101_adapt.zig:468). +//! resolveAttrByCode forwards it (adapter.zig:468). //! (b) DEFAULTED - the adapter synthesizes it without an S-57 alias (light //! sectors, topmark, featureName<-OBJNAM, zoneOfConfidence //! <-CATZOC, ...; ADAPTER_SYNTHESIZED below). @@ -37,10 +36,10 @@ const PERMITTED = "vendor/s101/permitted.json"; // per-class permitted enum valu const RULES_DIR = "vendor/S-101_Portrayal-Catalogue/PortrayalCatalog/Rules"; // Attributes the adapter synthesizes/derives WITHOUT a direct S-57 alias. -// Sourced verbatim from src/s100/s101_adapt.zig (2026-07-01); grow this when the +// Sourced verbatim from src/s101/adapter.zig (2026-07-01); grow this when the // adapter grows new synthesis. const ADAPTER_SYNTHESIZED = [_][]const u8{ - // orientation / clearance complexes (s101_adapt.zig:122-127, 494-501) + // orientation / clearance complexes (adapter.zig:122-127, 494-501) "orientation", "orientationValue", "verticalClearanceClosed", "verticalClearanceFixed", "verticalClearanceOpen", "verticalClearanceValue", @@ -48,13 +47,13 @@ const ADAPTER_SYNTHESIZED = [_][]const u8{ // Gate's HORCLR -> horizontalClearanceOpen (class-keyed; the other 8 classes that // reference it bind …Fixed, so its absence there is an expected-absence) "horizontalClearanceOpen", - // openingBridge synthesized from CATBRG 2..8 for BRIDGE->Bridge (s101_adapt.zig) + // openingBridge synthesized from CATBRG 2..8 for BRIDGE->Bridge (adapter.zig) "openingBridge", - // current velocity CURVEL -> speed.speedMaximum (s101_adapt.zig complex_from_simple) + // current velocity CURVEL -> speed.speedMaximum (adapter.zig complex_from_simple) "speed", "speedMaximum", "speedMinimum", // NATSUR list -> surfaceCharacteristics[].natureOfSurface for SeabedArea - // (s101_adapt.zig buildSurfaceCharacteristics; off-list values dropped) + // (adapter.zig buildSurfaceCharacteristics; off-list values dropped) "surfaceCharacteristics", // CATMOR {1,2} -> categoryOfDolphin for MORFAC-routed Dolphin (same coding) "categoryOfDolphin", @@ -62,9 +61,9 @@ const ADAPTER_SYNTHESIZED = [_][]const u8{ "valueOfLocalMagneticAnomaly", "magneticAnomalyValue", // inTheWater: producer spatial derivation (point over DEPARE and not over LNDARE), - // no S-57 source (s101_adapt.zig readsInTheWater); true-only, else absent + // no S-57 source (adapter.zig readsInTheWater); true-only, else absent "inTheWater", - // light sector + rhythm synthesis (s101_adapt.zig:376-431, 697-710) + // light sector + rhythm synthesis (adapter.zig:376-431, 697-710) "sectorCharacteristics", "lightSector", "sectorLimit", "sectorLimitOne", "sectorLimitTwo", "sectorBearing", @@ -72,21 +71,21 @@ const ADAPTER_SYNTHESIZED = [_][]const u8{ "lightCharacteristic", "signalGroup", "signalPeriod", "valueOfNominalRange", "lightVisibility", - // featureName from OBJNAM (s101_adapt.zig:466, 477-480) + // featureName from OBJNAM (adapter.zig:466, 477-480) "featureName", "name", "language", "nameUsage", - // zoneOfConfidence from M_QUAL CATZOC (s101_adapt.zig:482-489) + // zoneOfConfidence from M_QUAL CATZOC (adapter.zig:482-489) "zoneOfConfidence", "categoryOfZoneOfConfidenceInData", - // topmark fold (s101_adapt.zig:513-522, 641-643) + // topmark fold (adapter.zig:513-522, 641-643) "topmark", "topmarkDaymarkShape", - // QUAPOS aggregate -> feature attr (s101_adapt.zig:552-557) + // QUAPOS aggregate -> feature attr (adapter.zig:552-557) "qualityOfHorizontalMeasurement", - // depth range served for DepthArea/DepthContour (s101_adapt.zig:607-610) + // depth range served for DepthArea/DepthContour (adapter.zig:607-610) "depthRangeMinimumValue", - // underwater-hazard depths for UDWHAZ05/OBSTRN07/WRECKS05 (s101_adapt.zig:544-549): + // underwater-hazard depths for UDWHAZ05/OBSTRN07/WRECKS05 (adapter.zig:544-549): // defaultClearanceDepth always, surroundingDepth when the danger sits in a depth area "surroundingDepth", "defaultClearanceDepth", @@ -96,8 +95,8 @@ const ADAPTER_SYNTHESIZED = [_][]const u8{ // The coverage buckets above answer "is the attribute supplied?". These answer // "is the supplied VALUE valid S-101?". A slot is at-risk when the adapter // forwards a raw S-57 value that S-65 says must be remapped, dropped, or is off -// the S-101 allowable list. Keyed by S-57 acronym/object (matching S-65 and -// specs/s57-s101-conversion-gaps.md); translated to S-101 names at run time via +// the S-101 allowable list. Keyed by S-57 acronym/object (matching S-65); +// translated to S-101 names at run time via // the catalogue alias maps, so only attributes a rule ACTUALLY reads surface. const AttrNote = struct { acr: []const u8, note: []const u8 }; @@ -110,7 +109,7 @@ const VALUE_REMAP = [_]AttrNote{ }; // Acronyms whose VALUE_REMAP the adapter now applies before the value reaches a -// rule (s101_adapt.zig s65RemapValue / s65RemapQuapos): the prohibited/remapped +// rule (adapter.zig s65RemapValue / s65RemapQuapos): the prohibited/remapped // values are dropped or mapped, and every surviving value is inside the S-101 // allowable list — so the "remap" concern is retired. The per-object RESTRICTED // allowable-list axis, if any, is still reported (a separate concern). Grow this as @@ -143,11 +142,11 @@ const RESTRICTED = [_]AttrObjs{ .{ .acr = "BOYSHP", .objs = &.{"MORFAC"} }, }; -// `enforced` = the adapter drops this attribute write-side (s101_adapt.zig DROP_ATTRS / +// `enforced` = the adapter drops this attribute write-side (adapter.zig DROP_ATTRS / // isDroppedAttr), so the value never reaches a rule and the slot is NOT at-risk even if // a rule reads it. Defaults true: every §E pair below is enforced. A future §E entry the // adapter hasn't wired yet sets `.enforced = false` and still flags (the CI safety net, -// like VALUE_REMAP vs VALUE_REMAP_DONE). Keep this table in sync with s101_adapt.zig. +// like VALUE_REMAP vs VALUE_REMAP_DONE). Keep this table in sync with adapter.zig. const ObjAttr = struct { obj: []const u8, acr: []const u8, enforced: bool = true }; // Per-object attribute drops — "will not be converted" for that feature (gaps doc §E). // Enforced write-side by the adapter; the entries stay as the §E reference + safety net. diff --git a/tools/style.zig b/tools/style.zig index be8d750..0aa1958 100644 --- a/tools/style.zig +++ b/tools/style.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const assets = @import("assets"); +const style = @import("style"); const common = @import("common.zig"); const Flags = common.Flags; const usageErr = common.usageErr; @@ -15,7 +15,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { var colortables: ?[]const u8 = null; var out: ?[]const u8 = null; var scheme: []const u8 = "day"; - var opts = assets.StyleOpts{ .scheme = "day", .colortables_json = "" }; + var opts = style.Options{ .scheme = "day", .colortables_json = "" }; var f = Flags{ .args = args }; while (f.next()) |arg| { if (std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "--output")) { @@ -50,7 +50,7 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { try std.Io.Dir.cwd().readFileAlloc(io, ctf, a, .unlimited) else try colorTablesBytes(io, a, resolveCatalogDir(catalog)); - const style = try assets.styleJson(a, opts); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = style }); - std.debug.print("wrote {s} ({s}, {d} bytes)\n", .{ out_path, scheme, style.len }); + const style_json = try style.json(a, opts); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = style_json }); + std.debug.print("wrote {s} ({s}, {d} bytes)\n", .{ out_path, scheme, style_json.len }); } diff --git a/tools/tiledump.zig b/tools/tiledump.zig new file mode 100644 index 0000000..a7bead4 --- /dev/null +++ b/tools/tiledump.zig @@ -0,0 +1,125 @@ +//! tile57 tiledump — decode ONE raw (decompressed) vector +//! tile and summarise it: per-layer feature counts by geometry type, plus value +//! histograms for the properties that identify portrayal output (class, +//! symbol_name, ls). For debugging what a bake or the compositor actually put +//! in a tile. +//! +//! --geom CLASS switches to per-feature geometry mode: each polygon/line +//! feature with class=CLASS prints its properties and, per ring/part, the +//! point count, bbox, and (polygons) signed area — plus the full coordinate +//! list under --coords. For hunting degenerate geometry. + +const std = @import("std"); +const engine = @import("engine"); + +const Count = struct { pts: u32 = 0, lines: u32 = 0, polys: u32 = 0 }; + +pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8) !void { + if (args.len < 3) { + std.debug.print("usage: tile57 tiledump [--prop KEY]\n", .{}); + return; + } + const bytes = try std.Io.Dir.cwd().readFileAlloc(io, args[2], a, .unlimited); + var extra_prop: ?[]const u8 = null; + var geom_class: ?[]const u8 = null; + var coords = false; + var i: usize = 3; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--prop") and i + 1 < args.len) { + extra_prop = args[i + 1]; + i += 1; + } else if (std.mem.eql(u8, args[i], "--geom") and i + 1 < args.len) { + geom_class = args[i + 1]; + i += 1; + } else if (std.mem.eql(u8, args[i], "--coords")) { + coords = true; + } + } + + const layers = engine.mlt.decode(a, bytes) catch |mlt_err| blk: { + break :blk engine.mvt.decode(a, bytes) catch { + std.debug.print("not a decodable MLT ({s}) or MVT tile\n", .{@errorName(mlt_err)}); + return; + }; + }; + + if (geom_class) |cls| return dumpGeom(io, a, layers, cls, coords); + + var out = std.ArrayList(u8).empty; + for (layers) |layer| { + var c = Count{}; + var hist = std.StringHashMap(u32).init(a); + const keys = [_][]const u8{ "class", "symbol_name", "ls" }; + for (layer.features) |f| { + switch (f.geom_type) { + .point => c.pts += 1, + .linestring => c.lines += 1, + .polygon => c.polys += 1, + .unknown => {}, + } + for (f.properties) |p| { + const interesting = for (keys) |k| { + if (std.mem.eql(u8, p.key, k)) break true; + } else (extra_prop != null and std.mem.eql(u8, p.key, extra_prop.?)); + if (!interesting) continue; + switch (p.value) { + .string => |v| { + const tag = try std.fmt.allocPrint(a, "{s}={s}", .{ p.key, v }); + const g = try hist.getOrPutValue(tag, 0); + g.value_ptr.* += 1; + }, + else => {}, + } + } + } + try out.print(a, "layer {s}: {d} pts, {d} lines, {d} polys\n", .{ layer.name, c.pts, c.lines, c.polys }); + var it = hist.iterator(); + while (it.next()) |e| try out.print(a, " {s} x{d}\n", .{ e.key_ptr.*, e.value_ptr.* }); + } + std.Io.File.stdout().writeStreamingAll(io, out.items) catch {}; +} + +fn dumpGeom(io: std.Io, a: std.mem.Allocator, layers: []const engine.mvt.DecodedLayer, cls: []const u8, coords: bool) !void { + var out = std.ArrayList(u8).empty; + for (layers) |layer| { + for (layer.features, 0..) |f, fi| { + if (f.geom_type == .point) continue; + const matched = for (f.properties) |p| { + if (std.mem.eql(u8, p.key, "class") and p.value == .string and std.mem.eql(u8, p.value.string, cls)) break true; + } else false; + if (!matched) continue; + try out.print(a, "{s}[{d}] {s}", .{ layer.name, fi, @tagName(f.geom_type) }); + for (f.properties) |p| { + switch (p.value) { + .string => |v| try out.print(a, " {s}={s}", .{ p.key, v }), + .float => |v| try out.print(a, " {s}={d}", .{ p.key, v }), + .double => |v| try out.print(a, " {s}={d}", .{ p.key, v }), + .int => |v| try out.print(a, " {s}={d}", .{ p.key, v }), + .uint => |v| try out.print(a, " {s}={d}", .{ p.key, v }), + .boolean => |v| try out.print(a, " {s}={}", .{ p.key, v }), + } + } + try out.print(a, "\n", .{}); + for (f.parts, 0..) |ring, ri| { + var min_x: i32 = std.math.maxInt(i32); + var min_y: i32 = std.math.maxInt(i32); + var max_x: i32 = std.math.minInt(i32); + var max_y: i32 = std.math.minInt(i32); + var area2: i64 = 0; // twice the signed area (shoelace) + for (ring, 0..) |pt, pi| { + min_x = @min(min_x, pt.x); + min_y = @min(min_y, pt.y); + max_x = @max(max_x, pt.x); + max_y = @max(max_y, pt.y); + const nxt = ring[(pi + 1) % ring.len]; + area2 += @as(i64, pt.x) * nxt.y - @as(i64, nxt.x) * pt.y; + } + try out.print(a, " ring[{d}]: {d} pts, bbox [{d},{d}..{d},{d}], area2 {d}\n", .{ ri, ring.len, min_x, min_y, max_x, max_y, area2 }); + if (coords) { + for (ring) |pt| try out.print(a, " {d},{d}\n", .{ pt.x, pt.y }); + } + } + } + } + std.Io.File.stdout().writeStreamingAll(io, out.items) catch {}; +}