From b6a96eb22ea6fa693c007f564d3614e2768c71c2 Mon Sep 17 00:00:00 2001 From: Magnus Hedemark Date: Sat, 15 Aug 2026 21:13:11 -0400 Subject: [PATCH] feat: add ascii-city-engine skill (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add ascii-city-engine skill New skill teaching portable first-person colored-ASCII city engines and small GIS-derived city packs: - references/engine-architecture.md: terrain height function, ground- attached pedestrian physics (feet_z=terrain(x,y), step/slope limits, building-footprint collision with wall sliding), fisheye-corrected raycast-to-ASCII pipeline, glyph density ladder, deterministic color. - references/city-provider-contract.md: manifest + world-tile semantics, single-height-per-column v1 limit, reserved surface-graph extension. - references/gis-ingestion.md: USGS 3DEP/OSM/municipal source classes, meter-CRS reprojection, unit checks, 3.2 m/floor height fallback, provenance/confidence recording. - references/raleigh-poc.md: numeric downtown bbox, named sources with URLs, acquire/convert/validate commands, 4 human acceptance checks. - templates/: city-pack-manifest + world JSON Schemas. - scripts/validate-city-pack.py: pure-stdlib offline validator (78 rules). - assets/raleigh-downtown-sample/: committed coarse pack (30 real OSM buildings, 25 road/path surfaces, 78x64 10 m terrain) <= 64K. - assets/ascii-city-engine.html: dependency-free Canvas 2D engine. - evals/evals.json: 6 schema-valid cases incl. runtime-specificity and large-GIS-commit refusal boundaries. - README.md catalog entry. AI-assisted contribution (Hermes Agent, spec-driven-development pipeline). * chore: regenerate claude marketplace for ascii-city-engine * chore: regenerate codex plugin and llms.txt for ascii-city-engine * fix(ascii-city-engine): address droid-review findings on PR #321 1. [P2] Engine scaffold now reads manifest.json for spawn + first tile instead of hardcoding (315,385) — restores pack interchangeability. 2. [P2] Validator all_points() guards isinstance(dict) so structurally invalid tiles report FAIL instead of crashing with AttributeError. 3. [P2] raleigh-poc acceptance walk: correct duration/elevation math (~25s/~100m to ~100.9m at (315,505); ~95s to north edge ~103.0m). 4. [P3] known-limitations: match committed data — 5/30 explicit OSM height (0.88), 25 floor-estimated (0.72), not ~26%/0.55/0.35. 5. [P3] bbox: document tangent-plane 632x779m vs normalized 630x770m working extent instead of asserting 630x770 as the conversion result. 6. [P3][security] cap polygon vertices (2000), elevation cells (4M), feature count to prevent O(n^2) CPU-exhaustion on crafted packs. Verified: sample pack 79/79 PASS exit 0; broken fixture exit 1; droid AttributeError repro now FAILs cleanly; 5000-vertex footprint rejected in 58ms; node --check OK; validate-skills.rb 154 green; blocklist clean. --- .claude-plugin/marketplace.json | 9 + .codex-plugin/plugin.json | 1 + README.md | 4 + ascii-city-engine/README.md | 36 ++++ ascii-city-engine/SKILL.md | 42 +++++ .../assets/ascii-city-engine.html | 52 ++++++ .../assets/deliberately-broken-pack/README.md | 3 + .../deliberately-broken-pack/manifest.json | 8 + .../raleigh-downtown-sample/manifest.json | 53 ++++++ .../raleigh-downtown-sample/world/tile-0.json | 1 + ascii-city-engine/evals/evals.json | 76 +++++++++ .../references/city-provider-contract.md | 44 +++++ .../references/engine-architecture.md | 102 +++++++++++ ascii-city-engine/references/gis-ingestion.md | 50 ++++++ ascii-city-engine/references/raleigh-poc.md | 108 ++++++++++++ .../scripts/validate-city-pack.py | 159 ++++++++++++++++++ .../templates/city-pack-manifest.schema.json | 46 +++++ ascii-city-engine/templates/world.schema.json | 69 ++++++++ llms.txt | 1 + 19 files changed, 864 insertions(+) create mode 100644 ascii-city-engine/README.md create mode 100644 ascii-city-engine/SKILL.md create mode 100644 ascii-city-engine/assets/ascii-city-engine.html create mode 100644 ascii-city-engine/assets/deliberately-broken-pack/README.md create mode 100644 ascii-city-engine/assets/deliberately-broken-pack/manifest.json create mode 100644 ascii-city-engine/assets/raleigh-downtown-sample/manifest.json create mode 100644 ascii-city-engine/assets/raleigh-downtown-sample/world/tile-0.json create mode 100644 ascii-city-engine/evals/evals.json create mode 100644 ascii-city-engine/references/city-provider-contract.md create mode 100644 ascii-city-engine/references/engine-architecture.md create mode 100644 ascii-city-engine/references/gis-ingestion.md create mode 100644 ascii-city-engine/references/raleigh-poc.md create mode 100644 ascii-city-engine/scripts/validate-city-pack.py create mode 100644 ascii-city-engine/templates/city-pack-manifest.schema.json create mode 100644 ascii-city-engine/templates/world.schema.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 896b4d8..f0f687b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -86,6 +86,15 @@ "strict": false, "description": "Organize durable agent research outputs as summaries, analysis, and evidence dossiers. Use when producing multi-layer research artifacts or coordinating research handoffs." }, + { + "name": "ascii-city-engine", + "source": "./", + "skills": [ + "./ascii-city-engine" + ], + "strict": false, + "description": "Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-level interiors, general GIS analysis, or committing full-resolution GIS archives." + }, { "name": "autogen", "source": "./", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 2ac07be..4f1ed44 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -27,6 +27,7 @@ "./anydoc", "./api-design-and-evolution", "./artifact-pyramids", + "./ascii-city-engine", "./autogen", "./backend-engineering", "./binary-analysis", diff --git a/README.md b/README.md index 7401cd2..a0ea8f3 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Design, document, review, and evolve consumer-facing HTTP, GraphQL, RPC, event, Structure durable research work so people and agents can start with conclusions, then inspect the analysis and underlying evidence only when needed. +### [ascii-city-engine](ascii-city-engine/SKILL.md) + +Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Covers terrain-following pedestrian physics, raycast-to-character-grid rendering, a pluggable city-provider JSON contract with an offline validator, reproducible public-GIS ingestion, and a committed downtown Raleigh sample pack. Not for WebGL/3D games, multi-level interiors, or committing full-resolution GIS archives. + ### [autogen](autogen/SKILL.md) An expert-level skill for building **conversational multi-agent systems** with Microsoft's AutoGen framework. Unlike graph-based or role-based orchestration, AutoGen uses **agent-to-agent conversations** as the orchestration primitive. diff --git a/ascii-city-engine/README.md b/ascii-city-engine/README.md new file mode 100644 index 0000000..7b4fd5d --- /dev/null +++ b/ascii-city-engine/README.md @@ -0,0 +1,36 @@ +# ASCII City Engine + +## Why Install This Skill + +Turn public elevation, building, and street data into a portable, first-person colored-ASCII city without re-deriving coordinate, collision, and provenance rules. The skill keeps the renderer city-neutral and city packs interchangeable. + +## What You Get + +- A heightfield and solid-footprint engine architecture. +- JSON Schemas for manifests and world tiles. +- A deterministic offline city-pack validator. +- A pure browser, single-file Canvas 2D reference engine. +- A small redistributable downtown Raleigh sample with real public footprints. +- Reproducible GIS ingestion and Raleigh walkthrough references. + +## Quick Start + +```sh +python3 scripts/validate-city-pack.py assets/raleigh-downtown-sample +python3 -m http.server 8000 +``` + +Then open `http://localhost:8000/assets/ascii-city-engine.html`. The scaffold reads `assets/raleigh-downtown-sample/manifest.json` for the spawn point and first world tile; use W/S or arrow keys to move and A/D or left/right arrows to turn. + +## Triggers + +Load this skill for requests to build a colored ASCII city renderer, create a terrain-following first-person walker, define a pluggable city pack, ingest public GIS into the provided contract, or reproduce the Raleigh proof of concept. + +Do not load it for ordinary GIS analysis, WebGL/3D engines, multi-level interiors or tunnels, gameplay systems, or requests to commit full-resolution GIS archives. + +## Requirements + +- Python 3 standard library for validation and conversion examples. +- A current desktop browser with Canvas 2D and `fetch` support. +- Network access only while acquiring full-resolution public GIS data; validation and the committed sample run offline. +- Source licenses that permit redistribution, with attribution and retrieval metadata. diff --git a/ascii-city-engine/SKILL.md b/ascii-city-engine/SKILL.md new file mode 100644 index 0000000..da9f8fe --- /dev/null +++ b/ascii-city-engine/SKILL.md @@ -0,0 +1,42 @@ +--- +name: ascii-city-engine +description: Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-level interiors, general GIS analysis, or committing full-resolution GIS archives. +--- + +# ASCII City Engine + +Build a portable city experience in three layers: an engine, a city-provider contract, and a city pack. Keep city-specific facts out of engine code. + +## Workflow + +1. Define the pack boundary and local meter-based CRS. +2. Acquire elevation, building, and road/path data from public sources. +3. Verify downloads, licenses, units, and provenance before conversion. +4. Reproject all geometry to local meters and emit the manifest plus world tiles. +5. Validate the pack offline: + ```sh + python3 scripts/validate-city-pack.py path/to/city-pack + ``` +6. Load the pack's first tile in `assets/ascii-city-engine.html` and test movement, grade following, and solid footprints. +7. Record data limitations and human acceptance evidence. + +## Required invariants + +- Compute `feet_z` from terrain and `eye_z` from feet plus eye height. +- Reject null terrain, over-steep steps, and movement touching a solid footprint. +- Use deterministic building colors and clear missed rays every frame. +- Treat v1 as one ground height per `(x, y)` column; reserve, but do not implement, a surface graph. +- Keep the engine independent of any city, vendor, agent harness, or private infrastructure. +- Do not commit large or full-resolution source data. Commit only a small redistributable sample; document acquisition for the rest. + +## Reference routing + +- Read [references/engine-architecture.md](references/engine-architecture.md) for world math, rendering, collisions, and the scaffold contract. +- Read [references/city-provider-contract.md](references/city-provider-contract.md) when creating or validating a provider. +- Read [references/gis-ingestion.md](references/gis-ingestion.md) before downloading or converting GIS sources. +- Read [references/raleigh-poc.md](references/raleigh-poc.md) for the reproducible Raleigh proof of concept. +- Use [templates/city-pack-manifest.schema.json](templates/city-pack-manifest.schema.json) and [templates/world.schema.json](templates/world.schema.json) as authoritative data contracts. + +## Boundaries + +Use a different skill or implementation approach for WebGL/GPU rendering, mobile controls, combat, interiors, bridges with traversable space below, tunnels, or general-purpose geospatial analysis. If asked to specialize this skill for one agent runtime, preserve the portable core and put runtime integration outside this skill. If asked to commit large GIS binaries, refuse and provide reproducible download/conversion commands instead. diff --git a/ascii-city-engine/assets/ascii-city-engine.html b/ascii-city-engine/assets/ascii-city-engine.html new file mode 100644 index 0000000..dd1df93 --- /dev/null +++ b/ascii-city-engine/assets/ascii-city-engine.html @@ -0,0 +1,52 @@ + + + +ASCII City Engine
Loading city…
diff --git a/ascii-city-engine/assets/deliberately-broken-pack/README.md b/ascii-city-engine/assets/deliberately-broken-pack/README.md new file mode 100644 index 0000000..cfb5d92 --- /dev/null +++ b/ascii-city-engine/assets/deliberately-broken-pack/README.md @@ -0,0 +1,3 @@ +# Deliberately broken city pack + +This fixture is intentionally invalid. Its manifest has an empty tile list, an inverted x bound, and an invalid retrieval date. Run `python3 scripts/validate-city-pack.py assets/deliberately-broken-pack` from the skill directory to demonstrate deterministic `FAIL` output and a nonzero exit. diff --git a/ascii-city-engine/assets/deliberately-broken-pack/manifest.json b/ascii-city-engine/assets/deliberately-broken-pack/manifest.json new file mode 100644 index 0000000..d5ebb57 --- /dev/null +++ b/ascii-city-engine/assets/deliberately-broken-pack/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "broken-example", + "version": "0.1.0", + "crs": "local meters", + "bounds": {"min_x": 10, "min_y": 0, "max_x": 0, "max_y": 10}, + "tiles": [], + "provenance": [{"name": "Example", "url": "https://example.org/data", "license": "Example", "retrieved": "not-a-date"}] +} diff --git a/ascii-city-engine/assets/raleigh-downtown-sample/manifest.json b/ascii-city-engine/assets/raleigh-downtown-sample/manifest.json new file mode 100644 index 0000000..2c7593e --- /dev/null +++ b/ascii-city-engine/assets/raleigh-downtown-sample/manifest.json @@ -0,0 +1,53 @@ +{ + "name": "raleigh-downtown-sample", + "version": "0.1.0", + "crs": "local tangent plane; WGS84 origin [-78.6420, 35.7760]; x east/y north; units meters", + "bounds": { + "min_x": 0, + "min_y": 0, + "max_x": 630, + "max_y": 770 + }, + "tiles": [ + "world/tile-0.json" + ], + "spawn": { + "x": 315, + "y": 385, + "heading_deg": 0 + }, + "provenance": [ + { + "name": "USGS 3DEP Elevation Point Query Service", + "url": "https://epqs.nationalmap.gov/v1/json", + "license": "US public domain", + "retrieved": "2026-08-15", + "confidence": 0.72, + "notes": "Four corner observations bilinearly interpolated for the committed 10 m coarse sample." + }, + { + "name": "OpenStreetMap contributors via Overpass", + "url": "https://overpass-api.de/api/interpreter", + "license": "ODbL-1.0", + "retrieved": "2026-08-15", + "confidence": 0.86, + "notes": "Building footprints and tagged or floor-estimated heights." + }, + { + "name": "OpenStreetMap contributors via Overpass Kumi mirror", + "url": "https://overpass.kumi.systems/api/interpreter", + "license": "ODbL-1.0", + "retrieved": "2026-08-15", + "confidence": 0.9, + "notes": "Road, pedestrian, path, and sidewalk vectors." + }, + { + "name": "City of Raleigh Building Footprints", + "url": "https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer", + "license": "City of Raleigh Open Data custom terms; attribution required; verify before new redistribution", + "retrieved": "2026-08-15", + "confidence": 0.9, + "notes": "Municipal geometry cross-check; sample runtime footprints remain OSM-licensed records." + } + ] +} diff --git a/ascii-city-engine/assets/raleigh-downtown-sample/world/tile-0.json b/ascii-city-engine/assets/raleigh-downtown-sample/world/tile-0.json new file mode 100644 index 0000000..43341e9 --- /dev/null +++ b/ascii-city-engine/assets/raleigh-downtown-sample/world/tile-0.json @@ -0,0 +1 @@ +{"terrain":{"resolution_m":10,"origin":[0,0],"elevations":[[95.727,95.76,95.792,95.825,95.858,95.891,95.923,95.956,95.989,96.021,96.054,96.087,96.12,96.152,96.185,96.218,96.251,96.283,96.316,96.349,96.381,96.414,96.447,96.48,96.512,96.545,96.578,96.611,96.643,96.676,96.709,96.741,96.774,96.807,96.84,96.872,96.905,96.938,96.971,97.003,97.036,97.069,97.102,97.134,97.167,97.2,97.232,97.265,97.298,97.331,97.363,97.396,97.429,97.462,97.494,97.527,97.56,97.592,97.625,97.658,97.691,97.723,97.756,97.789],[95.803,95.836,95.869,95.902,95.935,95.967,96.0,96.033,96.066,96.099,96.132,96.165,96.198,96.231,96.264,96.297,96.33,96.362,96.395,96.428,96.461,96.494,96.527,96.56,96.593,96.626,96.659,96.692,96.725,96.758,96.79,96.823,96.856,96.889,96.922,96.955,96.988,97.021,97.054,97.087,97.12,97.153,97.185,97.218,97.251,97.284,97.317,97.35,97.383,97.416,97.449,97.482,97.515,97.548,97.581,97.613,97.646,97.679,97.712,97.745,97.778,97.811,97.844,97.877],[95.879,95.912,95.945,95.978,96.011,96.044,96.077,96.111,96.144,96.177,96.21,96.243,96.276,96.309,96.342,96.375,96.409,96.442,96.475,96.508,96.541,96.574,96.607,96.64,96.673,96.707,96.74,96.773,96.806,96.839,96.872,96.905,96.938,96.971,97.005,97.038,97.071,97.104,97.137,97.17,97.203,97.236,97.269,97.303,97.336,97.369,97.402,97.435,97.468,97.501,97.534,97.567,97.601,97.634,97.667,97.7,97.733,97.766,97.799,97.832,97.865,97.899,97.932,97.965],[95.955,95.988,96.021,96.055,96.088,96.121,96.155,96.188,96.221,96.255,96.288,96.321,96.354,96.388,96.421,96.454,96.488,96.521,96.554,96.588,96.621,96.654,96.687,96.721,96.754,96.787,96.821,96.854,96.887,96.921,96.954,96.987,97.02,97.054,97.087,97.12,97.154,97.187,97.22,97.254,97.287,97.32,97.353,97.387,97.42,97.453,97.487,97.52,97.553,97.587,97.62,97.653,97.686,97.72,97.753,97.786,97.82,97.853,97.886,97.92,97.953,97.986,98.019,98.053],[96.031,96.064,96.098,96.131,96.165,96.198,96.232,96.265,96.299,96.332,96.366,96.399,96.433,96.466,96.5,96.533,96.567,96.6,96.634,96.667,96.701,96.734,96.768,96.801,96.835,96.868,96.902,96.935,96.969,97.002,97.036,97.069,97.103,97.136,97.169,97.203,97.236,97.27,97.303,97.337,97.37,97.404,97.437,97.471,97.504,97.538,97.571,97.605,97.638,97.672,97.705,97.739,97.772,97.806,97.839,97.873,97.906,97.94,97.973,98.007,98.04,98.074,98.107,98.141],[96.107,96.14,96.174,96.208,96.242,96.275,96.309,96.343,96.376,96.41,96.444,96.477,96.511,96.545,96.578,96.612,96.646,96.679,96.713,96.747,96.78,96.814,96.848,96.881,96.915,96.949,96.982,97.016,97.05,97.084,97.117,97.151,97.185,97.218,97.252,97.286,97.319,97.353,97.387,97.42,97.454,97.488,97.521,97.555,97.589,97.622,97.656,97.69,97.723,97.757,97.791,97.824,97.858,97.892,97.926,97.959,97.993,98.027,98.06,98.094,98.128,98.161,98.195,98.229],[96.183,96.217,96.251,96.284,96.318,96.352,96.386,96.42,96.454,96.488,96.521,96.555,96.589,96.623,96.657,96.691,96.725,96.759,96.792,96.826,96.86,96.894,96.928,96.962,96.996,97.03,97.063,97.097,97.131,97.165,97.199,97.233,97.267,97.301,97.334,97.368,97.402,97.436,97.47,97.504,97.538,97.571,97.605,97.639,97.673,97.707,97.741,97.775,97.809,97.842,97.876,97.91,97.944,97.978,98.012,98.046,98.08,98.113,98.147,98.181,98.215,98.249,98.283,98.317],[96.259,96.293,96.327,96.361,96.395,96.429,96.463,96.497,96.531,96.565,96.599,96.633,96.668,96.702,96.736,96.77,96.804,96.838,96.872,96.906,96.94,96.974,97.008,97.042,97.076,97.11,97.144,97.178,97.212,97.247,97.281,97.315,97.349,97.383,97.417,97.451,97.485,97.519,97.553,97.587,97.621,97.655,97.689,97.723,97.757,97.791,97.826,97.86,97.894,97.928,97.962,97.996,98.03,98.064,98.098,98.132,98.166,98.2,98.234,98.268,98.302,98.336,98.371,98.405],[96.335,96.369,96.403,96.438,96.472,96.506,96.54,96.575,96.609,96.643,96.677,96.712,96.746,96.78,96.814,96.849,96.883,96.917,96.951,96.986,97.02,97.054,97.088,97.123,97.157,97.191,97.225,97.26,97.294,97.328,97.362,97.397,97.431,97.465,97.499,97.534,97.568,97.602,97.636,97.671,97.705,97.739,97.773,97.808,97.842,97.876,97.91,97.945,97.979,98.013,98.047,98.082,98.116,98.15,98.184,98.219,98.253,98.287,98.321,98.356,98.39,98.424,98.458,98.493],[96.411,96.445,96.48,96.514,96.549,96.583,96.617,96.652,96.686,96.721,96.755,96.79,96.824,96.858,96.893,96.927,96.962,96.996,97.031,97.065,97.1,97.134,97.168,97.203,97.237,97.272,97.306,97.341,97.375,97.41,97.444,97.478,97.513,97.547,97.582,97.616,97.651,97.685,97.719,97.754,97.788,97.823,97.857,97.892,97.926,97.961,97.995,98.029,98.064,98.098,98.133,98.167,98.202,98.236,98.271,98.305,98.339,98.374,98.408,98.443,98.477,98.512,98.546,98.581],[96.487,96.521,96.556,96.591,96.625,96.66,96.695,96.729,96.764,96.798,96.833,96.868,96.902,96.937,96.972,97.006,97.041,97.075,97.11,97.145,97.179,97.214,97.249,97.283,97.318,97.353,97.387,97.422,97.456,97.491,97.526,97.56,97.595,97.63,97.664,97.699,97.733,97.768,97.803,97.837,97.872,97.907,97.941,97.976,98.01,98.045,98.08,98.114,98.149,98.184,98.218,98.253,98.288,98.322,98.357,98.391,98.426,98.461,98.495,98.53,98.565,98.599,98.634,98.668],[96.563,96.598,96.632,96.667,96.702,96.737,96.772,96.806,96.841,96.876,96.911,96.946,96.981,97.015,97.05,97.085,97.12,97.155,97.19,97.224,97.259,97.294,97.329,97.364,97.398,97.433,97.468,97.503,97.538,97.573,97.607,97.642,97.677,97.712,97.747,97.781,97.816,97.851,97.886,97.921,97.956,97.99,98.025,98.06,98.095,98.13,98.164,98.199,98.234,98.269,98.304,98.339,98.373,98.408,98.443,98.478,98.513,98.548,98.582,98.617,98.652,98.687,98.722,98.756],[96.639,96.674,96.709,96.744,96.779,96.814,96.849,96.884,96.919,96.954,96.989,97.024,97.059,97.094,97.129,97.164,97.199,97.234,97.269,97.304,97.339,97.374,97.409,97.444,97.479,97.514,97.549,97.584,97.619,97.654,97.689,97.724,97.759,97.794,97.829,97.864,97.899,97.934,97.969,98.004,98.039,98.074,98.109,98.144,98.179,98.214,98.249,98.284,98.319,98.354,98.389,98.424,98.459,98.494,98.529,98.564,98.599,98.634,98.669,98.704,98.739,98.774,98.809,98.844],[96.715,96.75,96.785,96.82,96.856,96.891,96.926,96.961,96.996,97.032,97.067,97.102,97.137,97.172,97.208,97.243,97.278,97.313,97.348,97.384,97.419,97.454,97.489,97.524,97.56,97.595,97.63,97.665,97.7,97.736,97.771,97.806,97.841,97.876,97.912,97.947,97.982,98.017,98.052,98.088,98.123,98.158,98.193,98.228,98.264,98.299,98.334,98.369,98.404,98.44,98.475,98.51,98.545,98.58,98.616,98.651,98.686,98.721,98.756,98.792,98.827,98.862,98.897,98.932],[96.791,96.826,96.861,96.897,96.932,96.968,97.003,97.038,97.074,97.109,97.145,97.18,97.215,97.251,97.286,97.322,97.357,97.392,97.428,97.463,97.499,97.534,97.569,97.605,97.64,97.675,97.711,97.746,97.782,97.817,97.852,97.888,97.923,97.959,97.994,98.029,98.065,98.1,98.136,98.171,98.206,98.242,98.277,98.312,98.348,98.383,98.419,98.454,98.489,98.525,98.56,98.596,98.631,98.666,98.702,98.737,98.773,98.808,98.843,98.879,98.914,98.95,98.985,99.02],[96.867,96.902,96.938,96.973,97.009,97.045,97.08,97.116,97.151,97.187,97.223,97.258,97.294,97.329,97.365,97.4,97.436,97.472,97.507,97.543,97.578,97.614,97.649,97.685,97.721,97.756,97.792,97.827,97.863,97.899,97.934,97.97,98.005,98.041,98.076,98.112,98.148,98.183,98.219,98.254,98.29,98.325,98.361,98.397,98.432,98.468,98.503,98.539,98.575,98.61,98.646,98.681,98.717,98.752,98.788,98.824,98.859,98.895,98.93,98.966,99.002,99.037,99.073,99.108],[96.943,96.978,97.014,97.05,97.086,97.122,97.157,97.193,97.229,97.265,97.3,97.336,97.372,97.408,97.443,97.479,97.515,97.551,97.587,97.622,97.658,97.694,97.73,97.765,97.801,97.837,97.873,97.908,97.944,97.98,98.016,98.052,98.087,98.123,98.159,98.195,98.23,98.266,98.302,98.338,98.374,98.409,98.445,98.481,98.517,98.552,98.588,98.624,98.66,98.695,98.731,98.767,98.803,98.839,98.874,98.91,98.946,98.982,99.017,99.053,99.089,99.125,99.16,99.196],[97.019,97.055,97.091,97.127,97.163,97.198,97.234,97.27,97.306,97.342,97.378,97.414,97.45,97.486,97.522,97.558,97.594,97.63,97.666,97.702,97.738,97.774,97.81,97.846,97.882,97.918,97.954,97.99,98.026,98.062,98.097,98.133,98.169,98.205,98.241,98.277,98.313,98.349,98.385,98.421,98.457,98.493,98.529,98.565,98.601,98.637,98.673,98.709,98.745,98.781,98.817,98.853,98.889,98.925,98.961,98.997,99.032,99.068,99.104,99.14,99.176,99.212,99.248,99.284],[97.095,97.131,97.167,97.203,97.239,97.275,97.312,97.348,97.384,97.42,97.456,97.492,97.528,97.565,97.601,97.637,97.673,97.709,97.745,97.782,97.818,97.854,97.89,97.926,97.962,97.998,98.035,98.071,98.107,98.143,98.179,98.215,98.251,98.288,98.324,98.36,98.396,98.432,98.468,98.505,98.541,98.577,98.613,98.649,98.685,98.721,98.758,98.794,98.83,98.866,98.902,98.938,98.974,99.011,99.047,99.083,99.119,99.155,99.191,99.228,99.264,99.3,99.336,99.372],[97.171,97.207,97.243,97.28,97.316,97.352,97.389,97.425,97.461,97.498,97.534,97.57,97.607,97.643,97.679,97.716,97.752,97.788,97.825,97.861,97.897,97.934,97.97,98.006,98.043,98.079,98.116,98.152,98.188,98.225,98.261,98.297,98.334,98.37,98.406,98.443,98.479,98.515,98.552,98.588,98.624,98.661,98.697,98.733,98.77,98.806,98.842,98.879,98.915,98.951,98.988,99.024,99.06,99.097,99.133,99.169,99.206,99.242,99.278,99.315,99.351,99.387,99.424,99.46],[97.247,97.283,97.32,97.356,97.393,97.429,97.466,97.502,97.539,97.575,97.612,97.648,97.685,97.722,97.758,97.795,97.831,97.868,97.904,97.941,97.977,98.014,98.05,98.087,98.123,98.16,98.196,98.233,98.27,98.306,98.343,98.379,98.416,98.452,98.489,98.525,98.562,98.598,98.635,98.671,98.708,98.744,98.781,98.817,98.854,98.891,98.927,98.964,99.0,99.037,99.073,99.11,99.146,99.183,99.219,99.256,99.292,99.329,99.365,99.402,99.438,99.475,99.512,99.548],[97.323,97.359,97.396,97.433,97.47,97.506,97.543,97.58,97.616,97.653,97.69,97.727,97.763,97.8,97.837,97.873,97.91,97.947,97.984,98.02,98.057,98.094,98.13,98.167,98.204,98.241,98.277,98.314,98.351,98.388,98.424,98.461,98.498,98.534,98.571,98.608,98.645,98.681,98.718,98.755,98.791,98.828,98.865,98.902,98.938,98.975,99.012,99.049,99.085,99.122,99.159,99.195,99.232,99.269,99.306,99.342,99.379,99.416,99.452,99.489,99.526,99.563,99.599,99.636],[97.399,97.436,97.472,97.509,97.546,97.583,97.62,97.657,97.694,97.731,97.768,97.805,97.842,97.878,97.915,97.952,97.989,98.026,98.063,98.1,98.137,98.174,98.211,98.248,98.284,98.321,98.358,98.395,98.432,98.469,98.506,98.543,98.58,98.617,98.654,98.69,98.727,98.764,98.801,98.838,98.875,98.912,98.949,98.986,99.023,99.06,99.097,99.133,99.17,99.207,99.244,99.281,99.318,99.355,99.392,99.429,99.466,99.503,99.539,99.576,99.613,99.65,99.687,99.724],[97.475,97.512,97.549,97.586,97.623,97.66,97.697,97.734,97.771,97.809,97.846,97.883,97.92,97.957,97.994,98.031,98.068,98.105,98.142,98.18,98.217,98.254,98.291,98.328,98.365,98.402,98.439,98.476,98.513,98.551,98.588,98.625,98.662,98.699,98.736,98.773,98.81,98.847,98.884,98.922,98.959,98.996,99.033,99.07,99.107,99.144,99.181,99.218,99.255,99.293,99.33,99.367,99.404,99.441,99.478,99.515,99.552,99.589,99.626,99.664,99.701,99.738,99.775,99.812],[97.551,97.588,97.625,97.662,97.7,97.737,97.774,97.812,97.849,97.886,97.924,97.961,97.998,98.035,98.073,98.11,98.147,98.185,98.222,98.259,98.296,98.334,98.371,98.408,98.446,98.483,98.52,98.557,98.595,98.632,98.669,98.707,98.744,98.781,98.818,98.856,98.893,98.93,98.968,99.005,99.042,99.08,99.117,99.154,99.191,99.229,99.266,99.303,99.341,99.378,99.415,99.452,99.49,99.527,99.564,99.602,99.639,99.676,99.713,99.751,99.788,99.825,99.863,99.9],[97.627,97.664,97.702,97.739,97.777,97.814,97.851,97.889,97.926,97.964,98.001,98.039,98.076,98.114,98.151,98.189,98.226,98.264,98.301,98.339,98.376,98.414,98.451,98.489,98.526,98.564,98.601,98.639,98.676,98.714,98.751,98.788,98.826,98.863,98.901,98.938,98.976,99.013,99.051,99.088,99.126,99.163,99.201,99.238,99.276,99.313,99.351,99.388,99.426,99.463,99.501,99.538,99.576,99.613,99.651,99.688,99.726,99.763,99.8,99.838,99.875,99.913,99.95,99.988],[97.703,97.74,97.778,97.816,97.853,97.891,97.929,97.966,98.004,98.042,98.079,98.117,98.155,98.192,98.23,98.268,98.305,98.343,98.381,98.418,98.456,98.494,98.531,98.569,98.607,98.644,98.682,98.72,98.757,98.795,98.833,98.87,98.908,98.946,98.983,99.021,99.059,99.096,99.134,99.172,99.209,99.247,99.285,99.322,99.36,99.398,99.435,99.473,99.511,99.548,99.586,99.624,99.661,99.699,99.737,99.774,99.812,99.85,99.887,99.925,99.963,100.0,100.038,100.076],[97.779,97.816,97.854,97.892,97.93,97.968,98.006,98.044,98.081,98.119,98.157,98.195,98.233,98.271,98.309,98.346,98.384,98.422,98.46,98.498,98.536,98.574,98.612,98.649,98.687,98.725,98.763,98.801,98.839,98.877,98.914,98.952,98.99,99.028,99.066,99.104,99.142,99.179,99.217,99.255,99.293,99.331,99.369,99.407,99.444,99.482,99.52,99.558,99.596,99.634,99.672,99.709,99.747,99.785,99.823,99.861,99.899,99.937,99.974,100.012,100.05,100.088,100.126,100.164],[97.855,97.893,97.931,97.969,98.007,98.045,98.083,98.121,98.159,98.197,98.235,98.273,98.311,98.349,98.387,98.425,98.463,98.501,98.539,98.578,98.616,98.654,98.692,98.73,98.768,98.806,98.844,98.882,98.92,98.958,98.996,99.034,99.072,99.11,99.148,99.186,99.224,99.262,99.3,99.339,99.377,99.415,99.453,99.491,99.529,99.567,99.605,99.643,99.681,99.719,99.757,99.795,99.833,99.871,99.909,99.947,99.985,100.023,100.062,100.1,100.138,100.176,100.214,100.252],[97.931,97.969,98.007,98.045,98.084,98.122,98.16,98.198,98.236,98.275,98.313,98.351,98.389,98.428,98.466,98.504,98.542,98.581,98.619,98.657,98.695,98.734,98.772,98.81,98.848,98.887,98.925,98.963,99.001,99.04,99.078,99.116,99.154,99.192,99.231,99.269,99.307,99.345,99.384,99.422,99.46,99.498,99.537,99.575,99.613,99.651,99.69,99.728,99.766,99.804,99.843,99.881,99.919,99.957,99.996,100.034,100.072,100.11,100.149,100.187,100.225,100.263,100.301,100.34],[98.007,98.045,98.083,98.122,98.16,98.199,98.237,98.276,98.314,98.352,98.391,98.429,98.468,98.506,98.545,98.583,98.621,98.66,98.698,98.737,98.775,98.814,98.852,98.89,98.929,98.967,99.006,99.044,99.083,99.121,99.159,99.198,99.236,99.275,99.313,99.352,99.39,99.428,99.467,99.505,99.544,99.582,99.621,99.659,99.697,99.736,99.774,99.813,99.851,99.89,99.928,99.967,100.005,100.043,100.082,100.12,100.159,100.197,100.236,100.274,100.312,100.351,100.389,100.428],[98.083,98.121,98.16,98.198,98.237,98.276,98.314,98.353,98.391,98.43,98.469,98.507,98.546,98.585,98.623,98.662,98.7,98.739,98.778,98.816,98.855,98.894,98.932,98.971,99.009,99.048,99.087,99.125,99.164,99.203,99.241,99.28,99.318,99.357,99.396,99.434,99.473,99.512,99.55,99.589,99.627,99.666,99.705,99.743,99.782,99.82,99.859,99.898,99.936,99.975,100.014,100.052,100.091,100.129,100.168,100.207,100.245,100.284,100.323,100.361,100.4,100.438,100.477,100.516],[98.159,98.197,98.236,98.275,98.314,98.353,98.391,98.43,98.469,98.508,98.547,98.585,98.624,98.663,98.702,98.741,98.779,98.818,98.857,98.896,98.935,98.974,99.012,99.051,99.09,99.129,99.168,99.206,99.245,99.284,99.323,99.362,99.4,99.439,99.478,99.517,99.556,99.595,99.633,99.672,99.711,99.75,99.789,99.827,99.866,99.905,99.944,99.983,100.021,100.06,100.099,100.138,100.177,100.215,100.254,100.293,100.332,100.371,100.41,100.448,100.487,100.526,100.565,100.604],[98.235,98.274,98.313,98.352,98.391,98.43,98.469,98.508,98.547,98.586,98.625,98.664,98.703,98.742,98.781,98.82,98.859,98.898,98.937,98.976,99.015,99.054,99.093,99.132,99.171,99.21,99.249,99.288,99.327,99.366,99.405,99.444,99.483,99.522,99.561,99.6,99.639,99.678,99.717,99.756,99.795,99.834,99.873,99.912,99.951,99.99,100.029,100.068,100.107,100.146,100.185,100.224,100.263,100.302,100.341,100.38,100.419,100.458,100.497,100.536,100.575,100.614,100.653,100.692],[98.31,98.35,98.389,98.428,98.467,98.506,98.546,98.585,98.624,98.663,98.702,98.742,98.781,98.82,98.859,98.898,98.938,98.977,99.016,99.055,99.094,99.134,99.173,99.212,99.251,99.29,99.329,99.369,99.408,99.447,99.486,99.525,99.565,99.604,99.643,99.682,99.721,99.761,99.8,99.839,99.878,99.917,99.957,99.996,100.035,100.074,100.113,100.152,100.192,100.231,100.27,100.309,100.348,100.388,100.427,100.466,100.505,100.544,100.584,100.623,100.662,100.701,100.74,100.78],[98.386,98.426,98.465,98.505,98.544,98.583,98.623,98.662,98.702,98.741,98.78,98.82,98.859,98.898,98.938,98.977,99.017,99.056,99.095,99.135,99.174,99.213,99.253,99.292,99.332,99.371,99.41,99.45,99.489,99.529,99.568,99.607,99.647,99.686,99.725,99.765,99.804,99.844,99.883,99.922,99.962,100.001,100.04,100.08,100.119,100.159,100.198,100.237,100.277,100.316,100.356,100.395,100.434,100.474,100.513,100.552,100.592,100.631,100.671,100.71,100.749,100.789,100.828,100.867],[98.462,98.502,98.542,98.581,98.621,98.66,98.7,98.739,98.779,98.819,98.858,98.898,98.937,98.977,99.016,99.056,99.096,99.135,99.175,99.214,99.254,99.293,99.333,99.373,99.412,99.452,99.491,99.531,99.57,99.61,99.65,99.689,99.729,99.768,99.808,99.847,99.887,99.927,99.966,100.006,100.045,100.085,100.124,100.164,100.204,100.243,100.283,100.322,100.362,100.401,100.441,100.481,100.52,100.56,100.599,100.639,100.678,100.718,100.758,100.797,100.837,100.876,100.916,100.955],[98.538,98.578,98.618,98.658,98.698,98.737,98.777,98.817,98.857,98.896,98.936,98.976,99.016,99.055,99.095,99.135,99.175,99.214,99.254,99.294,99.334,99.373,99.413,99.453,99.493,99.532,99.572,99.612,99.652,99.692,99.731,99.771,99.811,99.851,99.89,99.93,99.97,100.01,100.049,100.089,100.129,100.169,100.208,100.248,100.288,100.328,100.367,100.407,100.447,100.487,100.527,100.566,100.606,100.646,100.686,100.725,100.765,100.805,100.845,100.884,100.924,100.964,101.004,101.043],[98.614,98.654,98.694,98.734,98.774,98.814,98.854,98.894,98.934,98.974,99.014,99.054,99.094,99.134,99.174,99.214,99.254,99.294,99.334,99.374,99.413,99.453,99.493,99.533,99.573,99.613,99.653,99.693,99.733,99.773,99.813,99.853,99.893,99.933,99.973,100.013,100.053,100.093,100.133,100.173,100.212,100.252,100.292,100.332,100.372,100.412,100.452,100.492,100.532,100.572,100.612,100.652,100.692,100.732,100.772,100.812,100.852,100.892,100.932,100.972,101.012,101.051,101.091,101.131],[98.69,98.731,98.771,98.811,98.851,98.891,98.931,98.971,99.012,99.052,99.092,99.132,99.172,99.212,99.252,99.293,99.333,99.373,99.413,99.453,99.493,99.533,99.574,99.614,99.654,99.694,99.734,99.774,99.814,99.855,99.895,99.935,99.975,100.015,100.055,100.095,100.136,100.176,100.216,100.256,100.296,100.336,100.376,100.417,100.457,100.497,100.537,100.577,100.617,100.657,100.697,100.738,100.778,100.818,100.858,100.898,100.938,100.978,101.019,101.059,101.099,101.139,101.179,101.219],[98.766,98.807,98.847,98.887,98.928,98.968,99.008,99.049,99.089,99.129,99.17,99.21,99.25,99.291,99.331,99.371,99.412,99.452,99.492,99.533,99.573,99.613,99.654,99.694,99.734,99.775,99.815,99.855,99.896,99.936,99.976,100.017,100.057,100.097,100.138,100.178,100.218,100.259,100.299,100.339,100.38,100.42,100.46,100.501,100.541,100.581,100.622,100.662,100.702,100.743,100.783,100.823,100.864,100.904,100.944,100.985,101.025,101.065,101.106,101.146,101.186,101.227,101.267,101.307],[98.842,98.883,98.923,98.964,99.005,99.045,99.086,99.126,99.167,99.207,99.248,99.288,99.329,99.369,99.41,99.45,99.491,99.531,99.572,99.612,99.653,99.693,99.734,99.774,99.815,99.855,99.896,99.936,99.977,100.018,100.058,100.099,100.139,100.18,100.22,100.261,100.301,100.342,100.382,100.423,100.463,100.504,100.544,100.585,100.625,100.666,100.706,100.747,100.787,100.828,100.868,100.909,100.95,100.99,101.031,101.071,101.112,101.152,101.193,101.233,101.274,101.314,101.355,101.395],[98.918,98.959,99.0,99.041,99.081,99.122,99.163,99.203,99.244,99.285,99.326,99.366,99.407,99.448,99.488,99.529,99.57,99.611,99.651,99.692,99.733,99.773,99.814,99.855,99.895,99.936,99.977,100.018,100.058,100.099,100.14,100.18,100.221,100.262,100.303,100.343,100.384,100.425,100.465,100.506,100.547,100.588,100.628,100.669,100.71,100.75,100.791,100.832,100.873,100.913,100.954,100.995,101.035,101.076,101.117,101.158,101.198,101.239,101.28,101.32,101.361,101.402,101.443,101.483],[98.994,99.035,99.076,99.117,99.158,99.199,99.24,99.281,99.322,99.363,99.403,99.444,99.485,99.526,99.567,99.608,99.649,99.69,99.731,99.772,99.812,99.853,99.894,99.935,99.976,100.017,100.058,100.099,100.14,100.181,100.221,100.262,100.303,100.344,100.385,100.426,100.467,100.508,100.549,100.59,100.63,100.671,100.712,100.753,100.794,100.835,100.876,100.917,100.958,100.999,101.039,101.08,101.121,101.162,101.203,101.244,101.285,101.326,101.367,101.408,101.448,101.489,101.53,101.571],[99.07,99.111,99.153,99.194,99.235,99.276,99.317,99.358,99.399,99.44,99.481,99.522,99.563,99.605,99.646,99.687,99.728,99.769,99.81,99.851,99.892,99.933,99.974,100.015,100.057,100.098,100.139,100.18,100.221,100.262,100.303,100.344,100.385,100.426,100.467,100.509,100.55,100.591,100.632,100.673,100.714,100.755,100.796,100.837,100.878,100.919,100.961,101.002,101.043,101.084,101.125,101.166,101.207,101.248,101.289,101.33,101.371,101.413,101.454,101.495,101.536,101.577,101.618,101.659],[99.146,99.188,99.229,99.27,99.312,99.353,99.394,99.435,99.477,99.518,99.559,99.6,99.642,99.683,99.724,99.766,99.807,99.848,99.889,99.931,99.972,100.013,100.055,100.096,100.137,100.178,100.22,100.261,100.302,100.344,100.385,100.426,100.467,100.509,100.55,100.591,100.633,100.674,100.715,100.756,100.798,100.839,100.88,100.921,100.963,101.004,101.045,101.087,101.128,101.169,101.21,101.252,101.293,101.334,101.376,101.417,101.458,101.499,101.541,101.582,101.623,101.665,101.706,101.747],[99.222,99.264,99.305,99.347,99.388,99.43,99.471,99.513,99.554,99.596,99.637,99.679,99.72,99.761,99.803,99.844,99.886,99.927,99.969,100.01,100.052,100.093,100.135,100.176,100.218,100.259,100.301,100.342,100.384,100.425,100.467,100.508,100.549,100.591,100.632,100.674,100.715,100.757,100.798,100.84,100.881,100.923,100.964,101.006,101.047,101.089,101.13,101.172,101.213,101.254,101.296,101.337,101.379,101.42,101.462,101.503,101.545,101.586,101.628,101.669,101.711,101.752,101.794,101.835],[99.298,99.34,99.382,99.423,99.465,99.507,99.548,99.59,99.632,99.673,99.715,99.757,99.798,99.84,99.882,99.923,99.965,100.007,100.048,100.09,100.132,100.173,100.215,100.257,100.298,100.34,100.382,100.423,100.465,100.507,100.548,100.59,100.632,100.673,100.715,100.757,100.798,100.84,100.881,100.923,100.965,101.006,101.048,101.09,101.131,101.173,101.215,101.256,101.298,101.34,101.381,101.423,101.465,101.506,101.548,101.59,101.631,101.673,101.715,101.756,101.798,101.84,101.881,101.923],[99.374,99.416,99.458,99.5,99.542,99.584,99.625,99.667,99.709,99.751,99.793,99.835,99.877,99.918,99.96,100.002,100.044,100.086,100.128,100.17,100.211,100.253,100.295,100.337,100.379,100.421,100.462,100.504,100.546,100.588,100.63,100.672,100.714,100.755,100.797,100.839,100.881,100.923,100.965,101.007,101.048,101.09,101.132,101.174,101.216,101.258,101.3,101.341,101.383,101.425,101.467,101.509,101.551,101.592,101.634,101.676,101.718,101.76,101.802,101.844,101.885,101.927,101.969,102.011],[99.45,99.492,99.534,99.576,99.619,99.661,99.703,99.745,99.787,99.829,99.871,99.913,99.955,99.997,100.039,100.081,100.123,100.165,100.207,100.249,100.291,100.333,100.375,100.417,100.459,100.501,100.543,100.585,100.627,100.67,100.712,100.754,100.796,100.838,100.88,100.922,100.964,101.006,101.048,101.09,101.132,101.174,101.216,101.258,101.3,101.342,101.384,101.426,101.468,101.51,101.552,101.594,101.636,101.679,101.721,101.763,101.805,101.847,101.889,101.931,101.973,102.015,102.057,102.099],[99.526,99.569,99.611,99.653,99.695,99.737,99.78,99.822,99.864,99.906,99.949,99.991,100.033,100.075,100.118,100.16,100.202,100.244,100.286,100.329,100.371,100.413,100.455,100.498,100.54,100.582,100.624,100.667,100.709,100.751,100.793,100.835,100.878,100.92,100.962,101.004,101.047,101.089,101.131,101.173,101.216,101.258,101.3,101.342,101.385,101.427,101.469,101.511,101.553,101.596,101.638,101.68,101.722,101.765,101.807,101.849,101.891,101.934,101.976,102.018,102.06,102.102,102.145,102.187],[99.602,99.645,99.687,99.73,99.772,99.814,99.857,99.899,99.942,99.984,100.027,100.069,100.111,100.154,100.196,100.239,100.281,100.323,100.366,100.408,100.451,100.493,100.536,100.578,100.62,100.663,100.705,100.748,100.79,100.833,100.875,100.917,100.96,101.002,101.045,101.087,101.129,101.172,101.214,101.257,101.299,101.342,101.384,101.426,101.469,101.511,101.554,101.596,101.639,101.681,101.723,101.766,101.808,101.851,101.893,101.935,101.978,102.02,102.063,102.105,102.148,102.19,102.232,102.275],[99.678,99.721,99.764,99.806,99.849,99.891,99.934,99.977,100.019,100.062,100.104,100.147,100.19,100.232,100.275,100.317,100.36,100.403,100.445,100.488,100.531,100.573,100.616,100.658,100.701,100.744,100.786,100.829,100.871,100.914,100.957,100.999,101.042,101.084,101.127,101.17,101.212,101.255,101.298,101.34,101.383,101.425,101.468,101.511,101.553,101.596,101.638,101.681,101.724,101.766,101.809,101.851,101.894,101.937,101.979,102.022,102.065,102.107,102.15,102.192,102.235,102.278,102.32,102.363],[99.754,99.797,99.84,99.883,99.926,99.968,100.011,100.054,100.097,100.14,100.182,100.225,100.268,100.311,100.354,100.396,100.439,100.482,100.525,100.568,100.61,100.653,100.696,100.739,100.782,100.824,100.867,100.91,100.953,100.996,101.038,101.081,101.124,101.167,101.21,101.252,101.295,101.338,101.381,101.424,101.466,101.509,101.552,101.595,101.638,101.68,101.723,101.766,101.809,101.852,101.894,101.937,101.98,102.023,102.066,102.108,102.151,102.194,102.237,102.28,102.322,102.365,102.408,102.451],[99.83,99.873,99.916,99.959,100.002,100.045,100.088,100.131,100.174,100.217,100.26,100.303,100.346,100.389,100.432,100.475,100.518,100.561,100.604,100.647,100.69,100.733,100.776,100.819,100.862,100.905,100.948,100.991,101.034,101.077,101.12,101.163,101.206,101.249,101.292,101.335,101.378,101.421,101.464,101.507,101.55,101.593,101.636,101.679,101.722,101.765,101.808,101.851,101.894,101.937,101.98,102.023,102.066,102.109,102.152,102.195,102.238,102.281,102.324,102.367,102.41,102.453,102.496,102.539],[99.906,99.949,99.993,100.036,100.079,100.122,100.165,100.209,100.252,100.295,100.338,100.381,100.424,100.468,100.511,100.554,100.597,100.64,100.684,100.727,100.77,100.813,100.856,100.899,100.943,100.986,101.029,101.072,101.115,101.159,101.202,101.245,101.288,101.331,101.374,101.418,101.461,101.504,101.547,101.59,101.634,101.677,101.72,101.763,101.806,101.849,101.893,101.936,101.979,102.022,102.065,102.109,102.152,102.195,102.238,102.281,102.324,102.368,102.411,102.454,102.497,102.54,102.584,102.627],[99.982,100.026,100.069,100.112,100.156,100.199,100.242,100.286,100.329,100.373,100.416,100.459,100.503,100.546,100.589,100.633,100.676,100.72,100.763,100.806,100.85,100.893,100.936,100.98,101.023,101.067,101.11,101.153,101.197,101.24,101.283,101.327,101.37,101.414,101.457,101.5,101.544,101.587,101.63,101.674,101.717,101.76,101.804,101.847,101.891,101.934,101.977,102.021,102.064,102.107,102.151,102.194,102.238,102.281,102.324,102.368,102.411,102.454,102.498,102.541,102.585,102.628,102.671,102.715],[100.058,100.102,100.145,100.189,100.233,100.276,100.32,100.363,100.407,100.45,100.494,100.537,100.581,100.625,100.668,100.712,100.755,100.799,100.842,100.886,100.929,100.973,101.017,101.06,101.104,101.147,101.191,101.234,101.278,101.322,101.365,101.409,101.452,101.496,101.539,101.583,101.626,101.67,101.714,101.757,101.801,101.844,101.888,101.931,101.975,102.019,102.062,102.106,102.149,102.193,102.236,102.28,102.323,102.367,102.411,102.454,102.498,102.541,102.585,102.628,102.672,102.716,102.759,102.803],[100.134,100.178,100.222,100.266,100.309,100.353,100.397,100.441,100.484,100.528,100.572,100.616,100.659,100.703,100.747,100.791,100.834,100.878,100.922,100.966,101.009,101.053,101.097,101.141,101.184,101.228,101.272,101.316,101.359,101.403,101.447,101.491,101.534,101.578,101.622,101.666,101.709,101.753,101.797,101.841,101.884,101.928,101.972,102.016,102.059,102.103,102.147,102.191,102.234,102.278,102.322,102.366,102.409,102.453,102.497,102.541,102.584,102.628,102.672,102.716,102.759,102.803,102.847,102.891],[100.21,100.254,100.298,100.342,100.386,100.43,100.474,100.518,100.562,100.606,100.65,100.694,100.738,100.781,100.825,100.869,100.913,100.957,101.001,101.045,101.089,101.133,101.177,101.221,101.265,101.309,101.353,101.397,101.441,101.485,101.528,101.572,101.616,101.66,101.704,101.748,101.792,101.836,101.88,101.924,101.968,102.012,102.056,102.1,102.144,102.188,102.232,102.275,102.319,102.363,102.407,102.451,102.495,102.539,102.583,102.627,102.671,102.715,102.759,102.803,102.847,102.891,102.935,102.979],[100.286,100.33,100.374,100.419,100.463,100.507,100.551,100.595,100.639,100.683,100.728,100.772,100.816,100.86,100.904,100.948,100.992,101.036,101.081,101.125,101.169,101.213,101.257,101.301,101.345,101.39,101.434,101.478,101.522,101.566,101.61,101.654,101.698,101.743,101.787,101.831,101.875,101.919,101.963,102.007,102.051,102.096,102.14,102.184,102.228,102.272,102.316,102.36,102.405,102.449,102.493,102.537,102.581,102.625,102.669,102.713,102.758,102.802,102.846,102.89,102.934,102.978,103.022,103.067],[100.362,100.407,100.451,100.495,100.54,100.584,100.628,100.672,100.717,100.761,100.805,100.85,100.894,100.938,100.983,101.027,101.071,101.116,101.16,101.204,101.249,101.293,101.337,101.382,101.426,101.47,101.515,101.559,101.603,101.648,101.692,101.736,101.781,101.825,101.869,101.913,101.958,102.002,102.046,102.091,102.135,102.179,102.224,102.268,102.312,102.357,102.401,102.445,102.49,102.534,102.578,102.623,102.667,102.711,102.756,102.8,102.844,102.889,102.933,102.977,103.022,103.066,103.11,103.154],[100.438,100.483,100.527,100.572,100.616,100.661,100.705,100.75,100.794,100.839,100.883,100.928,100.972,101.017,101.061,101.106,101.15,101.195,101.239,101.284,101.328,101.373,101.417,101.462,101.506,101.551,101.596,101.64,101.685,101.729,101.774,101.818,101.863,101.907,101.952,101.996,102.041,102.085,102.13,102.174,102.219,102.263,102.308,102.352,102.397,102.441,102.486,102.53,102.575,102.619,102.664,102.708,102.753,102.797,102.842,102.886,102.931,102.975,103.02,103.064,103.109,103.153,103.198,103.242],[100.514,100.559,100.604,100.648,100.693,100.738,100.782,100.827,100.872,100.917,100.961,101.006,101.051,101.095,101.14,101.185,101.229,101.274,101.319,101.364,101.408,101.453,101.498,101.542,101.587,101.632,101.676,101.721,101.766,101.811,101.855,101.9,101.945,101.989,102.034,102.079,102.123,102.168,102.213,102.258,102.302,102.347,102.392,102.436,102.481,102.526,102.57,102.615,102.66,102.705,102.749,102.794,102.839,102.883,102.928,102.973,103.017,103.062,103.107,103.152,103.196,103.241,103.286,103.33],[100.59,100.635,100.68,100.725,100.77,100.815,100.86,100.904,100.949,100.994,101.039,101.084,101.129,101.174,101.219,101.264,101.308,101.353,101.398,101.443,101.488,101.533,101.578,101.623,101.668,101.712,101.757,101.802,101.847,101.892,101.937,101.982,102.027,102.072,102.116,102.161,102.206,102.251,102.296,102.341,102.386,102.431,102.476,102.521,102.565,102.61,102.655,102.7,102.745,102.79,102.835,102.88,102.925,102.969,103.014,103.059,103.104,103.149,103.194,103.239,103.284,103.329,103.373,103.418],[100.666,100.711,100.756,100.801,100.847,100.892,100.937,100.982,101.027,101.072,101.117,101.162,101.207,101.252,101.297,101.342,101.387,101.433,101.478,101.523,101.568,101.613,101.658,101.703,101.748,101.793,101.838,101.883,101.928,101.974,102.019,102.064,102.109,102.154,102.199,102.244,102.289,102.334,102.379,102.424,102.469,102.515,102.56,102.605,102.65,102.695,102.74,102.785,102.83,102.875,102.92,102.965,103.01,103.055,103.101,103.146,103.191,103.236,103.281,103.326,103.371,103.416,103.461,103.506],[100.742,100.787,100.833,100.878,100.923,100.969,101.014,101.059,101.104,101.15,101.195,101.24,101.285,101.331,101.376,101.421,101.467,101.512,101.557,101.602,101.648,101.693,101.738,101.783,101.829,101.874,101.919,101.964,102.01,102.055,102.1,102.146,102.191,102.236,102.281,102.327,102.372,102.417,102.462,102.508,102.553,102.598,102.644,102.689,102.734,102.779,102.825,102.87,102.915,102.96,103.006,103.051,103.096,103.142,103.187,103.232,103.277,103.323,103.368,103.413,103.458,103.504,103.549,103.594],[100.818,100.864,100.909,100.955,101.0,101.045,101.091,101.136,101.182,101.227,101.273,101.318,101.364,101.409,101.455,101.5,101.546,101.591,101.636,101.682,101.727,101.773,101.818,101.864,101.909,101.955,102.0,102.046,102.091,102.137,102.182,102.227,102.273,102.318,102.364,102.409,102.455,102.5,102.546,102.591,102.637,102.682,102.728,102.773,102.818,102.864,102.909,102.955,103.0,103.046,103.091,103.137,103.182,103.228,103.273,103.319,103.364,103.409,103.455,103.5,103.546,103.591,103.637,103.682],[100.894,100.94,100.985,101.031,101.077,101.122,101.168,101.214,101.259,101.305,101.351,101.396,101.442,101.488,101.533,101.579,101.625,101.67,101.716,101.762,101.807,101.853,101.898,101.944,101.99,102.035,102.081,102.127,102.172,102.218,102.264,102.309,102.355,102.401,102.446,102.492,102.538,102.583,102.629,102.675,102.72,102.766,102.812,102.857,102.903,102.948,102.994,103.04,103.085,103.131,103.177,103.222,103.268,103.314,103.359,103.405,103.451,103.496,103.542,103.588,103.633,103.679,103.725,103.77],[100.97,101.016,101.062,101.108,101.153,101.199,101.245,101.291,101.337,101.383,101.429,101.474,101.52,101.566,101.612,101.658,101.704,101.749,101.795,101.841,101.887,101.933,101.979,102.024,102.07,102.116,102.162,102.208,102.254,102.3,102.345,102.391,102.437,102.483,102.529,102.575,102.62,102.666,102.712,102.758,102.804,102.85,102.895,102.941,102.987,103.033,103.079,103.125,103.171,103.216,103.262,103.308,103.354,103.4,103.446,103.491,103.537,103.583,103.629,103.675,103.721,103.766,103.812,103.858],[101.046,101.092,101.138,101.184,101.23,101.276,101.322,101.368,101.414,101.46,101.506,101.552,101.599,101.645,101.691,101.737,101.783,101.829,101.875,101.921,101.967,102.013,102.059,102.105,102.151,102.197,102.243,102.289,102.335,102.381,102.427,102.473,102.519,102.565,102.611,102.657,102.703,102.749,102.795,102.841,102.887,102.933,102.979,103.025,103.072,103.118,103.164,103.21,103.256,103.302,103.348,103.394,103.44,103.486,103.532,103.578,103.624,103.67,103.716,103.762,103.808,103.854,103.9,103.946],[101.122,101.168,101.215,101.261,101.307,101.353,101.399,101.446,101.492,101.538,101.584,101.631,101.677,101.723,101.769,101.815,101.862,101.908,101.954,102.0,102.047,102.093,102.139,102.185,102.231,102.278,102.324,102.37,102.416,102.463,102.509,102.555,102.601,102.647,102.694,102.74,102.786,102.832,102.879,102.925,102.971,103.017,103.063,103.11,103.156,103.202,103.248,103.295,103.341,103.387,103.433,103.479,103.526,103.572,103.618,103.664,103.711,103.757,103.803,103.849,103.895,103.942,103.988,104.034],[101.198,101.245,101.291,101.337,101.384,101.43,101.477,101.523,101.569,101.616,101.662,101.709,101.755,101.801,101.848,101.894,101.941,101.987,102.034,102.08,102.126,102.173,102.219,102.266,102.312,102.358,102.405,102.451,102.498,102.544,102.59,102.637,102.683,102.73,102.776,102.823,102.869,102.915,102.962,103.008,103.055,103.101,103.147,103.194,103.24,103.287,103.333,103.379,103.426,103.472,103.519,103.565,103.612,103.658,103.704,103.751,103.797,103.844,103.89,103.936,103.983,104.029,104.076,104.122],[101.274,101.321,101.367,101.414,101.46,101.507,101.554,101.6,101.647,101.694,101.74,101.787,101.833,101.88,101.927,101.973,102.02,102.066,102.113,102.16,102.206,102.253,102.299,102.346,102.393,102.439,102.486,102.532,102.579,102.626,102.672,102.719,102.765,102.812,102.859,102.905,102.952,102.998,103.045,103.092,103.138,103.185,103.231,103.278,103.325,103.371,103.418,103.464,103.511,103.558,103.604,103.651,103.697,103.744,103.791,103.837,103.884,103.93,103.977,104.024,104.07,104.117,104.163,104.21],[101.35,101.397,101.444,101.49,101.537,101.584,101.631,101.678,101.724,101.771,101.818,101.865,101.912,101.958,102.005,102.052,102.099,102.146,102.192,102.239,102.286,102.333,102.379,102.426,102.473,102.52,102.567,102.613,102.66,102.707,102.754,102.801,102.847,102.894,102.941,102.988,103.035,103.081,103.128,103.175,103.222,103.269,103.315,103.362,103.409,103.456,103.503,103.549,103.596,103.643,103.69,103.736,103.783,103.83,103.877,103.924,103.97,104.017,104.064,104.111,104.158,104.204,104.251,104.298],[101.426,101.473,101.52,101.567,101.614,101.661,101.708,101.755,101.802,101.849,101.896,101.943,101.99,102.037,102.084,102.131,102.178,102.225,102.272,102.319,102.366,102.413,102.46,102.507,102.554,102.601,102.648,102.695,102.742,102.789,102.836,102.883,102.929,102.976,103.023,103.07,103.117,103.164,103.211,103.258,103.305,103.352,103.399,103.446,103.493,103.54,103.587,103.634,103.681,103.728,103.775,103.822,103.869,103.916,103.963,104.01,104.057,104.104,104.151,104.198,104.245,104.292,104.339,104.386],[101.502,101.549,101.596,101.644,101.691,101.738,101.785,101.832,101.879,101.927,101.974,102.021,102.068,102.115,102.162,102.21,102.257,102.304,102.351,102.398,102.445,102.493,102.54,102.587,102.634,102.681,102.729,102.776,102.823,102.87,102.917,102.964,103.012,103.059,103.106,103.153,103.2,103.247,103.295,103.342,103.389,103.436,103.483,103.53,103.578,103.625,103.672,103.719,103.766,103.813,103.861,103.908,103.955,104.002,104.049,104.097,104.144,104.191,104.238,104.285,104.332,104.38,104.427,104.474],[101.578,101.625,101.673,101.72,101.767,101.815,101.862,101.91,101.957,102.004,102.052,102.099,102.146,102.194,102.241,102.288,102.336,102.383,102.431,102.478,102.525,102.573,102.62,102.667,102.715,102.762,102.809,102.857,102.904,102.952,102.999,103.046,103.094,103.141,103.188,103.236,103.283,103.33,103.378,103.425,103.473,103.52,103.567,103.615,103.662,103.709,103.757,103.804,103.851,103.899,103.946,103.994,104.041,104.088,104.136,104.183,104.23,104.278,104.325,104.372,104.42,104.467,104.514,104.562]],"provenance":{"source":"USGS 3DEP Elevation Point Query Service","url":"https://epqs.nationalmap.gov/v1/json","license":"US public domain","retrieved":"2026-08-15","confidence":0.72,"method":"Four real corner elevations, bilinear coarse interpolation"}},"buildings":[{"id":"osm-way-47663105","name":"","footprint":[[393.22,318.15],[393.11,314.87],[392.98,311.19],[384.54,311.55],[384.8,315.3],[385.08,318.41],[393.22,318.15]],"base_elev_m":99.63,"height_m":3.2,"color":"#4edad3","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-47663132","name":"Alexander Square Parking Deck","footprint":[[335.31,357.61],[334.95,346.8],[333.23,293.43],[332.95,284.74],[269.24,286.83],[271.59,359.68],[335.31,357.61]],"base_elev_m":99.44,"height_m":12.8,"color":"#4eda8d","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-47663137","name":"Wilmington Station Parking Deck","footprint":[[395.06,321.52],[395.27,326.99],[473.68,324.03],[472.77,299.97],[472.4,290.1],[473.71,290.04],[473.53,285.26],[393.85,288.24],[394.53,306.22],[370.85,307.1],[370.51,297.89],[365.13,298.09],[365.58,310.18],[394.6,309.09],[394.82,314.8],[395.06,321.52]],"base_elev_m":99.64,"height_m":22.4,"color":"#da4ea2","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-47663721","name":"Wake County Courthouse","footprint":[[213.19,51.32],[211.55,6.79],[167.13,8.45],[168.78,52.98],[213.19,51.32]],"base_elev_m":96.64,"height_m":65.0,"color":"#4e55da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"}},{"id":"osm-way-47732371","name":"Federal Building","footprint":[[226.14,93.4],[224.3,57.39],[176.09,59.88],[177.95,95.9],[226.14,93.4]],"base_elev_m":97.04,"height_m":12.8,"color":"#da714e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-47776682","name":"227 Fayetteville Street","footprint":[[326.16,160.67],[325.52,145.19],[263.09,147.78],[263.72,163.25],[326.16,160.67]],"base_elev_m":97.98,"height_m":35.2,"color":"#da4e94","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-47776706","name":"Moore Square Parking Deck","footprint":[[426.94,193.93],[426.16,178.68],[422.98,117.31],[414.72,117.68],[396.3,118.32],[393.67,118.53],[393.28,111.29],[388.28,111.55],[385.18,111.68],[386.01,131.54],[388.43,190.73],[406.59,189.98],[406.83,194.79],[426.94,193.93]],"base_elev_m":98.29,"height_m":25.6,"color":"#da4ebb","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-99029169","name":"Blount Street Parking Deck","footprint":[[341.9,46.95],[340.69,19.44],[340.52,13.44],[340.38,8.18],[365.39,7.27],[399.02,6.28],[412.2,5.72],[453.36,3.72],[468.5,3.09],[469.46,34.36],[469.92,44.82],[468.05,44.91],[460.81,45.23],[453.56,45.55],[453.39,42.0],[447.8,42.25],[412.59,43.82],[341.9,46.95]],"base_elev_m":97.31,"height_m":22.4,"color":"#da4eab","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-148163971","name":"State Employee's Credit Union Headquarters","footprint":[[148.52,700.22],[165.88,699.64],[166.36,679.85],[166.55,672.07],[166.74,664.1],[144.98,665.09],[145.56,671.74],[137.72,671.99],[129.97,672.26],[129.6,666.06],[109.5,666.54],[111.01,682.69],[148.25,681.05],[148.52,700.22]],"base_elev_m":101.54,"height_m":46.6,"color":"#4ec5da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"}},{"id":"osm-way-148163972","name":"North Carolina Museum of Natural Sciences: Nature Research Center","footprint":[[49.27,687.95],[50.14,708.25],[50.29,712.0],[50.67,720.76],[135.5,716.99],[135.26,713.1],[138.73,716.05],[141.84,717.03],[145.73,716.39],[148.23,713.32],[148.32,709.34],[146.22,705.57],[143.51,702.7],[147.33,702.4],[146.29,684.49],[94.12,686.12],[86.69,686.31],[49.27,687.95]],"base_elev_m":101.6,"height_m":12.8,"color":"#da6a4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-197551383","name":"","footprint":[[427.71,707.08],[430.01,707.08],[430.01,709.19],[427.71,709.19],[427.71,707.08]],"base_elev_m":103.09,"height_m":3.2,"color":"#dad34e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-199919512","name":"","footprint":[[168.24,705.52],[183.55,705.09],[193.47,704.8],[193.3,701.16],[191.74,701.18],[183.41,701.42],[147.33,702.4],[143.51,702.7],[146.22,705.57],[168.24,705.52]],"base_elev_m":101.87,"height_m":6.4,"color":"#b24eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-204963049","name":"Masonic Temple Building","footprint":[[267.1,279.61],[266.47,260.92],[293.19,260.02],[293.33,264.03],[293.83,278.71],[267.1,279.61]],"base_elev_m":98.84,"height_m":22.4,"color":"#4e8fda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632643","name":"First Citizens Bank","footprint":[[299.3,132.5],[298.31,113.25],[262.32,115.09],[263.3,134.35],[299.3,132.5]],"base_elev_m":97.68,"height_m":12.8,"color":"#4eda78","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632644","name":"First Church of Christ","footprint":[[266.38,241.46],[287.0,240.62],[286.77,235.16],[266.16,236.0],[266.38,241.46]],"base_elev_m":98.57,"height_m":6.4,"color":"#da4ea4","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632645","name":"American Underground @ Raleigh","footprint":[[297.84,191.94],[264.42,193.3],[264.73,200.8],[298.14,199.44],[297.84,191.94]],"base_elev_m":98.25,"height_m":9.6,"color":"#4eda86","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632647","name":"","footprint":[[265.62,222.73],[282.92,222.03],[282.7,216.61],[282.63,214.76],[265.32,215.46],[265.62,222.73]],"base_elev_m":98.4,"height_m":6.4,"color":"#4eda81","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632650","name":"One Exchange Plaza","footprint":[[264.42,193.3],[297.84,191.94],[328.56,190.68],[327.89,176.04],[325.5,174.0],[265.76,176.49],[263.73,178.93],[264.42,193.3]],"base_elev_m":98.19,"height_m":35.2,"color":"#4e53da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632652","name":"","footprint":[[305.29,132.2],[304.3,112.94],[298.31,113.25],[299.3,132.5],[305.29,132.2]],"base_elev_m":97.74,"height_m":6.4,"color":"#4e4eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632653","name":"","footprint":[[310.27,131.95],[309.29,112.69],[304.3,112.94],[305.29,132.2],[310.27,131.95]],"base_elev_m":97.75,"height_m":9.6,"color":"#dad74e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632654","name":"","footprint":[[315.65,239.45],[315.06,225.02],[308.81,225.28],[309.39,239.7],[315.65,239.45]],"base_elev_m":98.67,"height_m":3.2,"color":"#4e5cda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632655","name":"","footprint":[[318.35,131.54],[317.37,112.28],[309.29,112.69],[310.27,131.95],[318.35,131.54]],"base_elev_m":97.77,"height_m":9.6,"color":"#dac94e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632657","name":"","footprint":[[324.8,131.2],[323.82,111.94],[317.37,112.28],[318.35,131.54],[324.8,131.2]],"base_elev_m":97.8,"height_m":9.6,"color":"#dace4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632660","name":"Morning Times","footprint":[[298.11,240.17],[297.66,229.31],[286.56,229.76],[286.77,235.16],[287.0,240.62],[298.11,240.17]],"base_elev_m":98.61,"height_m":6.4,"color":"#da4e55","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632661","name":"Munjo Munjo","footprint":[[322.34,229.51],[322.73,239.16],[328.65,238.82],[330.54,237.03],[330.13,229.18],[322.34,229.51]],"base_elev_m":98.72,"height_m":6.4,"color":"#4edad5","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632662","name":"The Raleigh Times Bar","footprint":[[309.39,239.7],[308.81,225.28],[308.41,215.56],[300.67,215.87],[301.22,229.16],[297.66,229.31],[298.11,240.17],[309.39,239.7]],"base_elev_m":98.6,"height_m":6.4,"color":"#da4e51","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-209632663","name":"","footprint":[[328.87,198.18],[328.56,190.68],[297.84,191.94],[298.14,199.44],[328.87,198.18]],"base_elev_m":98.37,"height_m":3.2,"color":"#4edada","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels \u00d7 3.2 m/floor"}},{"id":"osm-way-268750429","name":"PNC Plaza","footprint":[[321.48,38.28],[321.69,44.17],[321.86,48.98],[322.07,54.77],[322.53,67.78],[322.72,73.17],[323.3,89.73],[274.32,91.48],[259.16,92.02],[259.06,89.27],[258.68,78.69],[258.57,75.46],[258.37,69.91],[257.91,56.97],[257.71,51.3],[257.34,40.54],[321.48,38.28]],"base_elev_m":97.2,"height_m":164.0,"color":"#614eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"}},{"id":"osm-way-297829069","name":"Wells Fargo Capitol Center","footprint":[[236.23,267.23],[236.91,284.01],[234.69,284.1],[234.83,287.63],[230.55,287.81],[230.74,292.53],[233.15,292.43],[233.5,301.46],[230.75,301.58],[230.91,305.74],[236.29,305.53],[236.41,308.61],[239.03,308.51],[239.44,318.77],[234.96,318.95],[235.11,322.85],[217.4,323.55],[217.35,322.18],[201.22,322.86],[201.29,324.55],[183.56,325.31],[181.32,273.31],[184.84,273.16],[184.65,268.75],[194.4,268.32],[194.5,270.63],[198.36,270.46],[198.48,273.07],[215.92,272.32],[215.83,270.15],[219.87,269.98],[219.79,267.91],[236.23,267.23]],"base_elev_m":98.79,"height_m":121.92,"color":"#b74eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"}},{"id":"osm-way-297829073","name":"SkyHouse Raleigh","footprint":[[413.01,59.85],[414.05,86.73],[469.37,84.57],[468.33,57.69],[461.11,57.98],[413.01,59.85]],"base_elev_m":97.74,"height_m":80.47,"color":"#4eda81","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"}}],"surfaces":[{"id":"osm-way-18916382","kind":"road","name":"Hillsborough Street","geometry":[[15.95,500.48],[25.68,500.06]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-187298754","kind":"road","name":"Fayetteville Street","geometry":[[259.15,401.61],[258.8,392.06],[255.73,327.2],[253.02,262.37],[252.63,251.73]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-805350747","kind":"road","name":"Fayetteville Street","geometry":[[246.04,103.72],[245.7,94.15],[242.8,27.99]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-823354147","kind":"road","name":"Fayetteville Street","geometry":[[252.63,251.73],[252.29,243.92],[249.3,176.06],[246.55,113.21],[246.04,103.72]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-1391371948","kind":"road","name":"Hillsborough Street","geometry":[[25.68,500.06],[33.15,499.65],[68.41,498.15],[72.06,497.99],[95.56,496.94],[106.94,496.44],[136.37,495.13],[163.95,493.89],[174.04,493.45]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18892120","kind":"road","name":"South Blount Street","geometry":[[495.51,391.22],[495.04,382.35],[491.46,299.26],[489.5,254.64],[489.37,251.7],[489.17,243.05]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18896767","kind":"road","name":"South Salisbury Street","geometry":[[174.04,493.45],[173.49,481.28],[171.22,430.21],[169.99,418.58],[169.77,415.95],[170.05,405.93]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18899030","kind":"road","name":"East Hargett Street","geometry":[[341.38,247.83],[331.47,248.3],[328.4,248.42],[319.26,248.81],[318.35,248.84],[265.03,251.21],[252.63,251.73]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18900792","kind":"road","name":"North Wilmington Street","geometry":[[351.88,486.57],[352.23,500.09],[355.25,566.81],[355.18,575.84]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18902781","kind":"road","name":"West Edenton Street","geometry":[[261.45,579.85],[221.63,581.74],[186.38,583.27],[178.05,583.48]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18903316","kind":"road","name":"Blake Street","geometry":[[560.55,19.18],[560.83,24.41]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18907049","kind":"road","name":"West Hargett Street","geometry":[[252.63,251.73],[239.07,252.33],[172.23,255.66],[162.95,256.15]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18907052","kind":"road","name":"East Morgan Street","geometry":[[259.15,401.61],[270.5,401.11],[338.25,396.38],[347.89,395.95]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18907675","kind":"sidewalk","name":"Market Plaza","geometry":[[259.48,140.14],[327.66,137.55]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18908804","kind":"road","name":"Wolfe Street","geometry":[[522.19,25.58],[487.99,26.94],[487.38,26.96],[479.71,27.2]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18909539","kind":"sidewalk","name":"Exchange Plaza","geometry":[[260.67,170.17],[328.81,167.58]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18910960","kind":"road","name":"North McDowell Street","geometry":[[29.7,590.1],[30.06,599.54],[31.59,632.57],[33.22,668.9],[33.75,681.52],[35.89,732.28],[36.21,739.26]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18911838","kind":"road","name":"Parham Street","geometry":[[524.96,91.38],[524.68,85.23],[523.56,58.31],[522.49,32.78],[522.19,25.58]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18912277","kind":"road","name":"New Bern Place","geometry":[[351.88,486.57],[359.86,486.23],[430.55,483.12],[452.72,482.14],[491.2,480.44],[499.65,480.14],[508.52,479.73]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18913362","kind":"road","name":"East Jones Street","geometry":[[267.33,728.83],[276.03,728.45],[331.65,725.69],[336.14,725.47],[352.88,724.63],[361.73,724.4]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18913654","kind":"road","name":"East Martin Street","geometry":[[246.04,103.72],[258.21,103.09],[325.28,100.0],[334.98,99.52]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18914424","kind":"road","name":"West Jones Street","geometry":[[184.5,732.92],[190.67,732.57],[267.33,728.83]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-18917127","kind":"road","name":"West Martin Street","geometry":[[156.89,106.97],[166.23,106.57],[168.46,106.52],[233.21,104.47],[246.04,103.72]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-98761429","kind":"road","name":"","geometry":[[432.61,175.91],[439.75,177.42],[444.06,178.11],[476.89,175.75],[486.29,175.18]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}},{"id":"osm-way-98761432","kind":"road","name":"","geometry":[[419.25,245.31],[418.94,236.97],[418.62,227.58],[418.44,222.39]],"walkable":true,"provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass.kumi.systems/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.9}}],"props":[]} diff --git a/ascii-city-engine/evals/evals.json b/ascii-city-engine/evals/evals.json new file mode 100644 index 0000000..8df5fad --- /dev/null +++ b/ascii-city-engine/evals/evals.json @@ -0,0 +1,76 @@ +{ + "schema_version": 1, + "skill_name": "ascii-city-engine", + "evals": [ + { + "id": "engine-physics-design", + "case_set": "dev", + "prompt": "I'm building a small browser-based first-person city walker and want colored ASCII output instead of polygons. The ground is not flat. Explain the world model and movement rules I should implement before I write the renderer.", + "expected_output": "A terrain-height-function design where the pedestrian's feet_z is derived from terrain(x, y), eye_z is feet plus eye height, proposed moves are rejected on null terrain, excessive step or slope, and building-footprint intersection, with wall sliding via axis-split retries.", + "assertions": [ + "Defines terrain as an interpolating height function returning a height or null, not as a free 3D position the player edits.", + "States the acceptance rule for a proposed move using a numeric max step and max slope, with rejection on null terrain.", + "Includes a building-footprint collision test that treats touching as collision and describes sliding along walls.", + "Keeps rendering (raycast to a character grid) separate from the physics model rather than deriving physics from pixels." + ] + }, + { + "id": "ascii-rendering-pipeline", + "case_set": "dev", + "prompt": "My raycaster produces fisheye-distorted walls and distant buildings flicker between colors frame to frame. How do I fix both, and how should distance map to glyph choice?", + "expected_output": "Fisheye correction via corrected distance d=t*cos(ray_angle-heading), deterministic per-building color (honor pack color else FNV-1a hash of the building ID to a hue), and a concrete near-to-far glyph density ladder with brightness falloff.", + "assertions": [ + "Names the fisheye cause and applies the cosine projection correction using the ray's angle relative to heading.", + "Requires stable building color across frames from a deterministic hash or pack-provided value, never per-frame randomness.", + "Provides an explicit glyph density ladder (for example @ % # + : .) tied to normalized distance thresholds.", + "Requires clearing columns that miss all geometry to sky or darkness every frame rather than leaving stale glyphs." + ] + }, + { + "id": "city-pack-from-gis", + "case_set": "dev", + "prompt": "I downloaded a GeoTIFF DEM and some OSM building data for my city. Walk me through turning that into a pack the engine can load.", + "expected_output": "The documented pipeline: verify downloads (HTTP status, size, parse, checksum, license, units), reproject to a local meter CRS before resampling, clip to bounds, estimate missing heights at 3.2 meters per floor with lowered confidence, emit manifest plus world tiles, and validate offline with the bundled script.", + "assertions": [ + "Requires reprojection to a meter-based CRS before grid resampling and explains why physics must not run in geographic degrees.", + "Requires an explicit unit check for elevation sources (feet versus meters) with the 0.3048 conversion.", + "Requires provenance on every terrain tile and building: source name, URL, license, retrieval date, and confidence in [0.0, 1.0].", + "Ends with running the offline validator and requires exit 0 before the pack is considered loadable." + ] + }, + { + "id": "bridge-request-v1-limit", + "case_set": "regression", + "prompt": "The city I want has a riverwalk passing under a bridge, and pedestrians should walk both levels. Can we model that with this engine?", + "expected_output": "A clear statement that v1 models exactly one ground height per (x, y) column and cannot represent a walkable bridge over walkable terrain, naming the reserved extensions.surface_graph as the future path, and offering in-scope alternatives (for example treating the bridge as a solid barrier or picking a single walkable level) rather than faking two heights in the terrain array.", + "assertions": [ + "States the single-height-per-column limitation explicitly instead of implying the terrain grid can hold two levels.", + "Names the reserved surface-graph extension point for future multi-level support.", + "Does not overload terrain elevations or surface records to fake a second walkable height.", + "Offers at least one honest v1 workaround and its tradeoff." + ] + }, + { + "id": "runtime-specific-request", + "case_set": "regression", + "prompt": "Make this skill specific to my agent runtime: reference its config paths and tool names directly in SKILL.md, and wire the engine to call the runtime's built-in file APIs.", + "expected_output": "A refusal of runtime-specific coupling in the skill itself: SKILL.md, references, templates, scripts, and assets stay runtime-neutral and portable, and any runtime integration is placed outside the skill (for example in a separate wrapper or host configuration), with the reason stated.", + "assertions": [ + "Declines to embed a specific agent runtime's config paths, tool names, or private APIs into the skill.", + "Explains that the skill's value is portability across Agent Skills clients.", + "Redirects runtime integration to a layer outside the skill directory." + ] + }, + { + "id": "commit-full-gis-archive", + "case_set": "regression", + "prompt": "Just commit the full 400 MB 3DEP DEM GeoTIFF and the complete OSM planet extract for Raleigh into the repository so everything works offline forever.", + "expected_output": "A refusal to commit large or full-resolution source data, with the committed-sample-plus-reproducible-commands alternative: a small coarse redistributable sample in assets, acquisition and conversion commands in the walkthrough, and license/attribution review before redistribution.", + "assertions": [ + "Refuses to commit large GIS binaries or full-resolution archives to the repository.", + "Explains the repository-size and license rationale for the refusal.", + "Provides the accepted alternative: small committed sample plus documented reproducible download and conversion commands." + ] + } + ] +} diff --git a/ascii-city-engine/references/city-provider-contract.md b/ascii-city-engine/references/city-provider-contract.md new file mode 100644 index 0000000..bb8cd84 --- /dev/null +++ b/ascii-city-engine/references/city-provider-contract.md @@ -0,0 +1,44 @@ +# City Provider Contract + +A city pack is a directory with `manifest.json` and one or more JSON world tiles. The engine reads only this contract; it must not branch on city name. + +## Manifest semantics + +Validate `manifest.json` against `../templates/city-pack-manifest.schema.json`. + +- `name`: stable lowercase pack identifier. +- `version`: pack release version. +- `crs`: local meter CRS description, including origin or EPSG code. +- `bounds`: inclusive `min_x`, `min_y`, `max_x`, `max_y` in that CRS. +- `tiles`: non-empty relative paths contained by the pack directory. +- `spawn`: optional walkable local coordinate and heading. +- `provenance`: source records with public URL, license, ISO retrieval date, and optional confidence. + +Paths must be relative, resolve inside the pack, and refer to regular JSON files. Manifest bounds must contain every tile's terrain, footprint, surface, and prop extent. + +## World-tile semantics + +Validate each tile against `../templates/world.schema.json`. + +- `terrain`: rectangular row-major elevations, meter resolution, local origin, and provenance. The extent is `[origin_x, origin_x+(columns-1)*resolution]` by `[origin_y, origin_y+(rows-1)*resolution]`. Null denotes a DEM void. +- `buildings`: globally unique IDs, simple closed-by-interpretation footprint polygons with at least three distinct vertices, meter base and positive height, stable color, and provenance/confidence. +- `surfaces`: IDs, kinds such as road/sidewalk/path/park/water, polyline or polygon geometry, and an explicit `walkable` flag. Surfaces annotate and render the ground; they do not replace terrain height. +- `props`: lightweight point objects such as trees, lights, and signs. + +A provider may split content into tiles, but duplicate building and surface IDs are forbidden. A consumer may stream or spatially index tiles without changing semantics. + +## V1 vertical limitation + +V1 models **exactly one ground height per `(x, y)` column**. It cannot represent a walkable bridge with walkable space beneath it, a tunnel under terrain, stacked interiors, or stairs between levels. Buildings are solid and non-enterable. + +The top-level `extensions.surface_graph` name is reserved for a future graph of distinct walkable surfaces, portals, and vertical relationships. Producers may preserve source hints there, but v1 engines must ignore the extension and must not claim bridge/tunnel traversal. Do not overload the terrain array or surface records to fake multiple heights. + +## Validation behavior + +Run: + +```sh +python3 scripts/validate-city-pack.py path/to/pack +``` + +The validator uses only the Python standard library and network-free local files. It prints one `PASS` or `FAIL` line per rule, then summary counts. Exit 0 means every rule passed; any schema, geometry, extent, provenance, or uniqueness failure exits 1. The fixture at `../assets/deliberately-broken-pack/` intentionally demonstrates failures and is not a usable provider. diff --git a/ascii-city-engine/references/engine-architecture.md b/ascii-city-engine/references/engine-architecture.md new file mode 100644 index 0000000..f5a0f54 --- /dev/null +++ b/ascii-city-engine/references/engine-architecture.md @@ -0,0 +1,102 @@ +# Engine Architecture + +## Coordinate and world model + +Use a right-handed local coordinate system in meters: `x` east, `y` north, `z` up. The terrain is a regular grid with origin `(ox, oy)`, spacing `r`, and elevations `H[row][col]`. Bilinear interpolation defines the ground: + +```text +terrain(x, y) -> z | null +u=(x-ox)/r; v=(y-oy)/r +z=(1-fu)(1-fv)H[j][i] + fu(1-fv)H[j][i+1] + + (1-fu)fvH[j+1][i] + fu*fvH[j+1][i+1] +``` + +Return `null` when outside the grid or any interpolation corner is null. A null cell is darkness and is not walkable. + +A building record contains: + +```text +{id, footprint:[[x,y],...], base_elev_m, height_m, color, + provenance:{source, url?, license?, retrieved?, confidence:0..1}} +``` + +The closed footprint is solid from `base_elev_m` to `base_elev_m + height_m`. The camera is `{x,y,heading_rad,fov_rad,eye_height_m}`; derive its vertical coordinates from terrain rather than storing an independent flying `z`. + +## Pedestrian physics + +At the accepted position: + +```text +feet_z = terrain(x, y) +eye_z = feet_z + eye_height_m +``` + +For proposed horizontal movement from `p0` to `p1`: + +```text +z0 = terrain(p0.x,p0.y); z1 = terrain(p1.x,p1.y) +if z1 is null: reject +height_delta = abs(z1-z0) +horizontal = hypot(p1.x-p0.x,p1.y-p0.y) +slope = height_delta / max(horizontal, 1e-9) +if height_delta > max_step_m or slope > max_slope: reject +if player circle at p1 touches/intersects any building footprint: reject +otherwise accept; feet_z=z1 +``` + +Use `max_step_m=0.45`, `max_slope=0.35`, and player radius `0.30 m` as starting values. Point-in-polygon plus minimum point-to-edge distance detects circle/polygon intersection. Treat equality as collision. If a diagonal proposal fails, test its x-only and y-only components separately to slide along walls without crossing them. + +## Raycast-to-ASCII-grid pipeline + +Render a fixed grid such as 100 columns by 36 rows on Canvas 2D. Each screen column casts a horizontal ray at angle + +```text +ray_angle = heading - FOV/2 + (column+0.5)/columns * FOV +ray(t) = camera_xy + t * [cos(ray_angle), sin(ray_angle)] +``` + +March `t` from a near plane to `max_distance` (for example, 150 m) in increments no larger than half the terrain resolution. At each sample: + +1. Evaluate terrain. Null means no ground sample. +2. Test whether the point lies in a building footprint. The first solid hit supplies wall distance, top elevation, stable color, and building ID. +3. Project vertical values using corrected distance `d=t*cos(ray_angle-heading)` to avoid fish-eye distortion: + `screen_y = horizon - focal_px*(world_z-eye_z)/max(d,epsilon)` where `focal_px=(columns/2)/tan(FOV/2)`. +4. Fill the visible wall interval and terrain below it into character-grid cells only when nearer than that cell's depth buffer. +5. If the ray reaches maximum distance without geometry, explicitly reset the column to sky/darkness; never reuse a previous frame. + +Pseudocode: + +```text +clear(chars=' ', fg=sky, depth=infinity) +for sx in columns: + ray = make_ray(sx) + for t in march(near,max_distance,step): + sample terrain and solids + project visible span + for sy in span: if corrected_distance < depth[sy][sx]: write cell + if opaque building covers remaining span: break +paint character grid to Canvas 2D +``` + +## Glyph density, brightness, and color + +Map normalized distance `q=clamp(d/max_distance,0,1)` to a concrete near-to-far density ladder: + +```text +q < .12: '@' +q < .25: '%' +q < .42: '#' +q < .60: '+' +q < .78: ':' +otherwise: '.' +``` + +Scale foreground brightness with `brightness=0.25+0.75*(1-q)^1.4`; terrain can use `.,:;+=xX#@` according to both distance and local slope. Glyph cell dimensions stay fixed; apparent size changes through projected vertical span, while density and brightness fall with distance. + +For stable building color, honor a valid pack-provided color. Otherwise hash the UTF-8 building ID with FNV-1a, select hue `hash % 360`, and use fixed saturation/lightness such as `hsl(hue 65% 58%)`. Recompute deterministically or cache by ID; never pick random colors per frame. Adjacent equal colors are acceptable. + +Dynamic cars or pedestrians may be depth-tested billboard ASCII sprites anchored at `terrain(x,y)`. They are optional and do not alter static collision. + +## Reference scaffold + +`../assets/ascii-city-engine.html` is a single dependency-free file. It reads the pack's `manifest.json` for the spawn coordinate and first world tile (falling back to `world/tile-0.json`), builds terrain and footprint indices, applies the movement rules above, and renders colored characters on Canvas 2D. Serve the repository root over HTTP because browsers commonly block `fetch` from `file://` URLs. diff --git a/ascii-city-engine/references/gis-ingestion.md b/ascii-city-engine/references/gis-ingestion.md new file mode 100644 index 0000000..10b1a96 --- /dev/null +++ b/ascii-city-engine/references/gis-ingestion.md @@ -0,0 +1,50 @@ +# GIS Ingestion + +## Public source classes + +Use sources whose terms permit the intended output and record the exact layer, not just a portal home page. + +| Class | Named source | URL | Typical use | +|---|---|---|---| +| Elevation DEM | USGS 3D Elevation Program through The National Map | https://apps.nationalmap.gov/ and https://epqs.nationalmap.gov/v1/json | Ground elevations in meters; the point service is suitable for a coarse sample and 3DEP downloads for full packs. | +| Building footprints/heights | OpenStreetMap contributors through Overpass API | https://overpass-api.de/api/interpreter | Footprints plus `height` or `building:levels` tags under ODbL 1.0. | +| Municipal building footprints | City of Raleigh Building Footprints FeatureServer | https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer | Authoritative annual aerial-derived planimetry; review the portal's custom license before redistribution. | +| Road/path vectors | OpenStreetMap contributors through Overpass API | https://overpass-api.de/api/interpreter | `highway=*`, sidewalk, footway, path, and name tags under ODbL 1.0. | + +For Raleigh, municipal footprint geometry may be cross-checked with OSM; OSM height tags can enrich municipal records. Keep both source claims when geometry and height come from different places. + +## Reproducible pipeline + +1. Fix a WGS84 bounding box and source retrieval date. +2. Download each response to a separate immutable source file. Fail on HTTP errors. Verify content type, nontrivial byte size, parseability, and a SHA-256 checksum before conversion; an HTML error page or truncated response must not proceed. +3. Read source metadata and verify horizontal CRS, vertical datum, and elevation units. Convert feet to meters explicitly with `meters=feet*0.3048`; never infer units silently. +4. Reproject all geometry into a local meter-based CRS before resampling. UTM Zone 17N (`EPSG:32617`) is suitable for Raleigh; a documented local tangent plane also works for a small pack. Geographic degrees have different east-west and north-south scales and cannot support correct meter-based step, slope, distance, FOV, or collision calculations. +5. Clip features to bounds. For a building crossing the boundary, clip its polygon and record `clipped: true`. +6. Resample the DEM to a regular grid. Preserve voids as JSON `null`; do not interpolate across unknown coverage without marking the result and lowering confidence. +7. Normalize building polygons, reject self-intersections, and convert heights to meters. A nonpositive height is missing. When height is absent, use `height_m = floors * 3.2` with **3.2 meters per floor**. If floors are also absent, use a documented class default. Mark estimated values, lower confidence (for example 0.55 for tagged floors or 0.35 for a class default), and retain the method. +8. If height sources disagree, select the value with the higher documented confidence and retain the losing observation in provenance notes. +9. Normalize roads, sidewalks, and paths into surface geometry and set `walkable` deliberately rather than inferring it at runtime. +10. Emit the manifest and tile JSON, then run the offline validator. + +## Provenance requirements + +Every terrain tile and emitted building must carry or reference: + +- source name; +- exact public source URL; +- license or terms identifier; +- retrieval date in `YYYY-MM-DD` form; +- confidence from `0.0` through `1.0`. + +Record whether geometry, elevation, and height came from separate sources. The manifest lists all pack-level sources; feature-level provenance identifies the source actually used. Confidence is an evidence judgment, not positional precision. Also retain checksums in acquisition notes even when the runtime schema does not require them. + +## Local tangent-plane fallback + +For a compact box centered at `(lon0,lat0)`, stdlib-only conversion can use: + +```text +x = radians(lon-lon0) * 6378137 * cos(radians(lat0)) +y = radians(lat-lat0) * 6378137 +``` + +Document this as an equirectangular local tangent approximation, use it only over a small area, and keep the origin in `crs`. For larger packs, use a vetted projection library and EPSG CRS. diff --git a/ascii-city-engine/references/raleigh-poc.md b/ascii-city-engine/references/raleigh-poc.md new file mode 100644 index 0000000..e56171a --- /dev/null +++ b/ascii-city-engine/references/raleigh-poc.md @@ -0,0 +1,108 @@ +# Raleigh Proof of Concept + +This walkthrough reproduces the committed `../assets/raleigh-downtown-sample/` pack and scales it up to a fuller downtown pack. The committed sample is intentionally coarse (10 m terrain, simplified footprints, <= 2 MB) so the repository stays light; the full pipeline below is what you run for a denser local pack. + +## Downtown bounding box (authoritative for this POC) + +WGS84 (EPSG:4326), south-west to north-east: + +```text +min: lon -78.6420, lat 35.7760 +max: lon -78.6350, lat 35.7830 +``` + +This box covers the Fayetteville Street core, the State Capitol grounds, Nash and Moore Squares' west edges, and the Hillsborough Street corridor. Through the tangent-plane conversion in `gis-ingestion.md` the box measures about 632 m east-west by 779 m north-south; the committed sample's manifest bounds and terrain grid are normalized to an exact 630 m by 770 m working extent (78 rows x 64 cols at 10 m) that sits inside it, so the documented formula and the shipped grid differ by roughly 1-2 m at the edges. It is deliberately compact so the sample stays small and every acceptance check is walkable in minutes. Expand symmetrically (for example to `-78.6520..-78.6280, 35.7680..35.7910`) for a fuller downtown pack. + +Local pack CRS: equirectangular tangent plane, origin `[-78.6420, 35.7760]`, x east / y north, meters (see `gis-ingestion.md` §Local tangent-plane fallback). + +## Data sources (all retrieved 2026-08-15) + +| Layer | Source | URL | License | Role | +|---|---|---|---|---| +| Elevation | USGS 3DEP Elevation Point Query Service | `https://epqs.nationalmap.gov/v1/json` | US public domain | Terrain heights | +| Buildings (footprints + heights) | OpenStreetMap via Overpass | `https://overpass-api.de/api/interpreter` | ODbL 1.0 | Runtime footprints, tagged/estimated heights | +| Roads, sidewalks, paths | OpenStreetMap via Overpass (Kumi mirror) | `https://overpass.kumi.systems/api/interpreter` | ODbL 1.0 | Walkable surface network | +| Buildings (municipal cross-check) | City of Raleigh Building Footprints FeatureServer | `https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer` | City of Raleigh Open Data terms; attribution required | Geometry verification; heights absent | + +Observed yields on 2026-08-15 for the authoritative box: OSM returned 160 building ways, 41 carrying `height` or `building:levels`; the Raleigh FeatureServer returned 95 footprint polygons in the same box; EPQS center observation `(-78.6385, 35.7795)` = 106.517 m. EPQS corner observations for the sample grid: 95.727, 97.789, 101.578, 104.562 m — about 9 m of relief across the box. + +## Acquisition commands + +Run from anywhere; outputs land in `/tmp`. Each step must succeed before the next (fail on HTTP error, then sanity-check the parse). + +```sh +# 1. OSM buildings (footprints + height/level tags) +curl --fail --silent --show-error --get \ + --data-urlencode 'data=[out:json][timeout:60];(way[building](35.7760,-78.6420,35.7830,-78.6350););out tags geom;' \ + --output /tmp/raleigh-buildings.json \ + https://overpass-api.de/api/interpreter + +# 2. OSM roads/sidewalks/paths (primary endpoint 504s under load; use a mirror) +curl --fail --silent --show-error --get \ + --data-urlencode 'data=[out:json][timeout:90];way[highway](35.7760,-78.6420,35.7830,-78.6350);out tags geom;' \ + --output /tmp/raleigh-roads.json \ + https://overpass.kumi.systems/api/interpreter + +# 3. Municipal footprints (cross-check; 1 page is enough at this box size) +curl --fail --silent --show-error --get \ + --data-urlencode 'where=1=1' \ + --data-urlencode 'geometry=-78.6420,35.7760,-78.6350,35.7830' \ + --data-urlencode 'geometryType=esriGeometryEnvelope' \ + --data-urlencode 'inSR=4326' \ + --data-urlencode 'spatialRel=esriSpatialRelIntersects' \ + --data-urlencode 'outFields=*' \ + --data-urlencode 'returnGeometry=true' \ + --data-urlencode 'f=geojson' \ + --output /tmp/raleigh-city-footprints.geojson \ + 'https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer/0/query' + +# 4. Elevation: one EPQS call per grid node for a coarse sample; +# for a full pack, download the 3DEP 1/3 arc-second DEM for the tile and resample. +curl --fail --silent --show-error \ + --output /tmp/raleigh-elevation-center.json \ + 'https://epqs.nationalmap.gov/v1/json?x=-78.6385&y=35.7795&units=Meters&wkid=4326&includeDate=False' +``` + +Verification per `gis-ingestion.md`: check HTTP status, byte size, JSON parseability, and record a SHA-256 per file before conversion. If the City of Raleigh URL has moved, search the portal (`https://data.raleighnc.gov`) for "Building Footprints" and record the new service URL plus your retrieval date; the committed sample keeps this POC runnable regardless. + +## Conversion + +Reproject lon/lat to the local tangent plane (`gis-ingestion.md` §fallback), clip to the box, normalize polygons, estimate missing heights at **3.2 m per floor** with lowered confidence, emit `manifest.json` plus `world/tile-0.json`, then validate: + +```sh +python3 scripts/validate-city-pack.py assets/raleigh-downtown-sample +``` + +Expected tail of a good run: + +```text +PASS tile[0].content.bounds — world/tile-0.json +PASS buildings.unique-ids +PASS surfaces.unique-ids +PASS manifest.spawn.bounds +SUMMARY rules_passed=... rules_failed=0 buildings=30 terrain_extent=[0.0, 0.0]..[630.0, 770.0] surfaces=25 +``` + +The deliberately-broken fixture must fail: + +```sh +python3 scripts/validate-city-pack.py assets/deliberately-broken-pack +# FAIL manifest.bounds / manifest.tiles.nonempty / manifest.provenance ... ; exit 1 +``` + +## Human acceptance checks (run after any acquisition) + +Serve the repo root (`python3 -m http.server 8000`) and open `http://localhost:8000/assets/ascii-city-engine.html`. + +1. **Load.** The status line reads `30 buildings · 25 surfaces` with no error text, and colored building walls appear against dark sky. +2. **Spawn.** Camera starts at local `(315, 385)` — mid-box, on the Fayetteville Street axis. The HUD elevation reads about 99–100 m. +3. **Walk >= 100 m with a grade.** Face north (heading 0) and hold W for ~25 seconds. At the scaffold's 4 m/s you cover ~100 m, from spawn (315, 385) to about (315, 505), and the HUD elevation climbs from ~99.9 m to ~100.9 m — a clear, continuous rise. Keep holding W to ~95 s (~380 m) to reach the north edge of the grid at y=770, where the x=315 column tops out at ~103.0 m. The steepest actual street grade in this box is the **Hillsborough Street rise west of the Capitol** — crossing it, you should see the horizon and building bases shift as `feet_z` follows terrain; movement is never rejected on this gentle grade. +4. **Solid footprint.** Turn toward the nearest large wall (the commercial block south-west of spawn, an OSM `COMMERCIAL` footprint) and hold W into it. Forward motion stops at the wall face; strafing (A/D while holding W) slides along it. You cannot pass through or under it. + +Re-run the validator after every fresh acquisition; if a data update moves a footprint onto the documented spawn, pick a new walkable spawn inside bounds and update `manifest.json` before publishing the pack. + +## Known limitations of the committed sample + +- Terrain is bilinear interpolation of four EPQS corner observations — smooth and correct in trend, but it cannot show curb-level detail. A full pack should resample the 3DEP DEM at 2–5 m. +- Only 5 of 30 buildings (16.7%) carry an explicit OSM `height` tag (confidence 0.88); the other 25 use the `OSM building:levels × 3.2 m/floor` estimate (confidence 0.72). The 0.55/0.35 fallback tiers in `gis-ingestion.md` step 7 apply to lower-evidence cases than this sample contains. Skyline proportions are right; individual roof elevations may not be. +- Raleigh municipal footprints are used as a geometry cross-check only; runtime footprints stay on ODbL-licensed OSM geometry so the sample remains redistributable with attribution. diff --git a/ascii-city-engine/scripts/validate-city-pack.py b/ascii-city-engine/scripts/validate-city-pack.py new file mode 100644 index 0000000..459942f --- /dev/null +++ b/ascii-city-engine/scripts/validate-city-pack.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Validate an ASCII city pack using only the Python standard library.""" +from __future__ import annotations + +import json +import math +import re +import sys +from datetime import date +from pathlib import Path + +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$") + +# Resource caps: bound CPU/memory spent on any single pack so a crafted or +# corrupt pack cannot trigger unbounded work in the O(n^2) geometry checks. +MAX_POLYGON_VERTICES = 2000 # per building footprint or surface geometry +MAX_ELEVATION_CELLS = 4_000_000 # per terrain grid (e.g. 2000x2000) +MAX_FEATURES_PER_TILE = 100_000 # buildings + surfaces + props combined + +class Report: + def __init__(self): self.passed = 0; self.failed = 0 + def rule(self, name, ok, detail=""): + self.passed += bool(ok); self.failed += not ok + suffix = f" — {detail}" if detail else "" + print(f"{'PASS' if ok else 'FAIL'} {name}{suffix}") + +def load_json(path, report, label): + try: + data = json.loads(path.read_text(encoding="utf-8")) + report.rule(label, True, str(path)) + return data + except Exception as exc: + report.rule(label, False, f"{path}: {exc}") + return None + +def valid_date(value): + if not isinstance(value, str) or not DATE_RE.fullmatch(value): return False + try: date.fromisoformat(value); return True + except ValueError: return False + +def finite_number(value): return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) +def point(value): return isinstance(value, list) and len(value) == 2 and all(finite_number(v) for v in value) +def orient(a,b,c): return (b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]) +def on_segment(a,b,p): return min(a[0],b[0]) <= p[0] <= max(a[0],b[0]) and min(a[1],b[1]) <= p[1] <= max(a[1],b[1]) +def intersects(a,b,c,d): + o1,o2,o3,o4 = orient(a,b,c),orient(a,b,d),orient(c,d,a),orient(c,d,b) + if ((o1>0 and o2<0) or (o1<0 and o2>0)) and ((o3>0 and o4<0) or (o3<0 and o4>0)): return True + return any(abs(o)<1e-9 and on_segment(x,y,p) for o,x,y,p in ((o1,a,b,c),(o2,a,b,d),(o3,c,d,a),(o4,c,d,b))) +def simple_polygon(poly): + if not isinstance(poly,list) or len(poly)<3 or not all(point(p) for p in poly): return False + if len(poly) > MAX_POLYGON_VERTICES: return False + pts = poly[:-1] if poly[0] == poly[-1] else poly + if len(pts)<3 or len({tuple(p) for p in pts})<3 or abs(sum(pts[i][0]*pts[(i+1)%len(pts)][1]-pts[(i+1)%len(pts)][0]*pts[i][1] for i in range(len(pts))))<1e-9: return False + n=len(pts) + for i in range(n): + for j in range(i+1,n): + if j in (i,(i+1)%n) or i in (j,(j+1)%n): continue + if i==0 and j==n-1: continue + if intersects(pts[i],pts[(i+1)%n],pts[j],pts[(j+1)%n]): return False + return True + +def valid_provenance(p, manifest=False): + if not isinstance(p,dict): return False + source_key = "name" if manifest else "source" + required=(source_key,"url","license","retrieved") + if not all(isinstance(p.get(k),str) and p[k] for k in required): return False + if not p["url"].startswith("https://") or not valid_date(p["retrieved"]): return False + if not manifest and not (finite_number(p.get("confidence")) and 0 <= p["confidence"] <= 1): return False + if "confidence" in p and not (finite_number(p["confidence"]) and 0 <= p["confidence"] <= 1): return False + return True + +def inside(bounds,x,y): return bounds["min_x"] <= x <= bounds["max_x"] and bounds["min_y"] <= y <= bounds["max_y"] +def all_points(tile): + for b in tile.get("buildings", []): + if isinstance(b, dict): + yield from b.get("footprint", []) + for s in tile.get("surfaces", []): + if isinstance(s, dict): + yield from s.get("geometry", []) + for p in tile.get("props", []): + if isinstance(p, dict): + yield [p.get("x"), p.get("y")] + +def main(argv): + report=Report(); pack=Path(argv[1]).resolve() if len(argv)==2 else None + report.rule("pack.directory", bool(pack and pack.is_dir()), str(pack) if pack else "usage: validate-city-pack.py ") + if not pack or not pack.is_dir(): return 1 + root=Path(__file__).resolve().parents[1] + ms=load_json(root/"templates/city-pack-manifest.schema.json",report,"schema.manifest.load") + ws=load_json(root/"templates/world.schema.json",report,"schema.world.load") + report.rule("schema.manifest.required-contract", bool(ms and set(("name","version","crs","bounds","tiles","provenance")) <= set(ms.get("required",[])))) + report.rule("schema.world.required-contract", bool(ws and set(("terrain","buildings","surfaces","props")) <= set(ws.get("required",[])))) + manifest=load_json(pack/"manifest.json",report,"manifest.parse") + if not isinstance(manifest,dict): return 1 + required=("name","version","crs","bounds","tiles","provenance") + report.rule("manifest.required", all(k in manifest for k in required), ", ".join(required)) + report.rule("manifest.identity", isinstance(manifest.get("name"),str) and bool(manifest["name"]) and isinstance(manifest.get("version"),str) and bool(re.fullmatch(r"\d+\.\d+\.\d+",manifest["version"])) and isinstance(manifest.get("crs"),str) and bool(manifest["crs"])) + b=manifest.get("bounds") + bounds_ok=isinstance(b,dict) and all(finite_number(b.get(k)) for k in ("min_x","min_y","max_x","max_y")) and b["min_x"]=2 and all(isinstance(row,list) and len(row)>=2 for row in elev) and len({len(row) for row in elev})==1 and all(v is None or finite_number(v) for row in elev for v in row) + cells=sum(len(row) for row in elev) if isinstance(elev,list) else 0 + report.rule(f"tile[{idx}].terrain.cells",cells<=MAX_ELEVATION_CELLS,f"{cells} cells") + terrain_ok=finite_number(res) and res>0 and point(origin) and rectangular and cells<=MAX_ELEVATION_CELLS and valid_provenance(terrain.get("provenance")) + report.rule(f"tile[{idx}].terrain",terrain_ok,"rectangular grid, meter resolution, provenance") + if terrain_ok: + ext=(origin[0],origin[1],origin[0]+(len(elev[0])-1)*res,origin[1]+(len(elev)-1)*res); terrain_extents.append(ext) + report.rule(f"tile[{idx}].terrain.bounds",bool(bounds_ok and inside(b,ext[0],ext[1]) and inside(b,ext[2],ext[3])),str(ext)) + buildings=tile.get("buildings",[]); building_count += len(buildings) if isinstance(buildings,list) else 0 + b_ok=isinstance(buildings,list) + for j,item in enumerate(buildings if isinstance(buildings,list) else []): + ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and simple_polygon(item.get("footprint")) and finite_number(item.get("base_elev_m")) and finite_number(item.get("height_m")) and item["height_m"]>0 and isinstance(item.get("color"),str) and bool(COLOR_RE.fullmatch(item["color"])) and valid_provenance(item.get("provenance")) + report.rule(f"tile[{idx}].building[{j}]",ok,item.get("id","missing id") if isinstance(item,dict) else "not object"); b_ok &= ok + if isinstance(item,dict) and isinstance(item.get("id"),str): building_ids.append(item["id"]) + report.rule(f"tile[{idx}].buildings",b_ok,f"count={len(buildings) if isinstance(buildings,list) else 0}") + surfaces=tile.get("surfaces",[]); surface_count += len(surfaces) if isinstance(surfaces,list) else 0 + s_ok=isinstance(surfaces,list) + for j,item in enumerate(surfaces if isinstance(surfaces,list) else []): + ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and isinstance(item.get("kind"),str) and bool(item["kind"]) and isinstance(item.get("walkable"),bool) and isinstance(item.get("geometry"),list) and 2<=len(item["geometry"])<=MAX_POLYGON_VERTICES and all(point(p) for p in item["geometry"]) and valid_provenance(item.get("provenance")) + report.rule(f"tile[{idx}].surface[{j}]",ok,item.get("id","missing id") if isinstance(item,dict) else "not object"); s_ok &= ok + if isinstance(item,dict) and isinstance(item.get("id"),str): surface_ids.append(item["id"]) + report.rule(f"tile[{idx}].surfaces",s_ok,f"count={len(surfaces) if isinstance(surfaces,list) else 0}") + props=tile.get("props",[]) + p_ok=isinstance(props,list) and all(isinstance(p,dict) and isinstance(p.get("id"),str) and isinstance(p.get("kind"),str) and finite_number(p.get("x")) and finite_number(p.get("y")) for p in props) + report.rule(f"tile[{idx}].props",p_ok,f"count={len(props) if isinstance(props,list) else 0}") + extents_ok=bool(bounds_ok) and all(point(p) and inside(b,p[0],p[1]) for p in all_points(tile)) + report.rule(f"tile[{idx}].content.bounds",extents_ok,rel) + duplicates=sorted({x for x in building_ids if building_ids.count(x)>1}) + report.rule("buildings.unique-ids",not duplicates,", ".join(duplicates)) + surface_dupes=sorted({x for x in surface_ids if surface_ids.count(x)>1}) + report.rule("surfaces.unique-ids",not surface_dupes,", ".join(surface_dupes)) + if isinstance(manifest.get("spawn"),dict) and bounds_ok: + s=manifest["spawn"]; report.rule("manifest.spawn.bounds",finite_number(s.get("x")) and finite_number(s.get("y")) and finite_number(s.get("heading_deg")) and inside(b,s["x"],s["y"])) + if terrain_extents: + minx=min(e[0] for e in terrain_extents); miny=min(e[1] for e in terrain_extents); maxx=max(e[2] for e in terrain_extents); maxy=max(e[3] for e in terrain_extents) + extent=f"[{minx:.1f}, {miny:.1f}]..[{maxx:.1f}, {maxy:.1f}]" + else: extent="none" + print(f"SUMMARY rules_passed={report.passed} rules_failed={report.failed} buildings={building_count} terrain_extent={extent} surfaces={surface_count}") + return 0 if report.failed==0 else 1 + +if __name__ == "__main__": raise SystemExit(main(sys.argv)) diff --git a/ascii-city-engine/templates/city-pack-manifest.schema.json b/ascii-city-engine/templates/city-pack-manifest.schema.json new file mode 100644 index 0000000..3afb3cd --- /dev/null +++ b/ascii-city-engine/templates/city-pack-manifest.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "city-pack-manifest.schema.json", + "title": "ASCII city pack manifest", + "type": "object", + "required": ["name", "version", "crs", "bounds", "tiles", "provenance"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"}, + "crs": {"type": "string", "minLength": 1}, + "bounds": { + "type": "object", + "required": ["min_x", "min_y", "max_x", "max_y"], + "properties": { + "min_x": {"type": "number"}, "min_y": {"type": "number"}, + "max_x": {"type": "number"}, "max_y": {"type": "number"} + }, + "additionalProperties": false + }, + "tiles": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "spawn": { + "type": "object", + "required": ["x", "y", "heading_deg"], + "properties": {"x": {"type": "number"}, "y": {"type": "number"}, "heading_deg": {"type": "number"}}, + "additionalProperties": false + }, + "provenance": { + "type": "array", "minItems": 1, + "items": { + "type": "object", + "required": ["name", "url", "license", "retrieved"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "url": {"type": "string", "pattern": "^https://"}, + "license": {"type": "string", "minLength": 1}, + "retrieved": {"type": "string", "format": "date"}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + "notes": {"type": "string"} + }, + "additionalProperties": true + } + }, + "extensions": {"type": "object"} + }, + "additionalProperties": false +} diff --git a/ascii-city-engine/templates/world.schema.json b/ascii-city-engine/templates/world.schema.json new file mode 100644 index 0000000..e9cc8aa --- /dev/null +++ b/ascii-city-engine/templates/world.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "world.schema.json", + "title": "ASCII city world tile", + "type": "object", + "required": ["terrain", "buildings", "surfaces", "props"], + "$defs": { + "point": {"type": "array", "prefixItems": [{"type": "number"}, {"type": "number"}], "minItems": 2, "maxItems": 2}, + "provenance": { + "type": "object", + "required": ["source", "url", "license", "retrieved", "confidence"], + "properties": { + "source": {"type": "string", "minLength": 1}, + "url": {"type": "string", "pattern": "^https://"}, + "license": {"type": "string", "minLength": 1}, + "retrieved": {"type": "string", "format": "date"}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + "method": {"type": "string"}, "clipped": {"type": "boolean"} + }, + "additionalProperties": true + } + }, + "properties": { + "terrain": { + "type": "object", "required": ["resolution_m", "origin", "elevations", "provenance"], + "properties": { + "resolution_m": {"type": "number", "exclusiveMinimum": 0}, + "origin": {"$ref": "#/$defs/point"}, + "elevations": {"type": "array", "minItems": 2, "items": {"type": "array", "minItems": 2, "items": {"type": ["number", "null"]}}}, + "provenance": {"$ref": "#/$defs/provenance"} + }, "additionalProperties": false + }, + "buildings": { + "type": "array", + "items": { + "type": "object", "required": ["id", "footprint", "base_elev_m", "height_m", "color", "provenance"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "name": {"type": "string"}, + "footprint": {"type": "array", "minItems": 3, "items": {"$ref": "#/$defs/point"}}, + "base_elev_m": {"type": "number"}, "height_m": {"type": "number", "exclusiveMinimum": 0}, + "color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"}, + "provenance": {"$ref": "#/$defs/provenance"} + }, "additionalProperties": true + } + }, + "surfaces": { + "type": "array", + "items": { + "type": "object", "required": ["id", "kind", "geometry", "walkable", "provenance"], + "properties": { + "id": {"type": "string", "minLength": 1}, "kind": {"type": "string", "minLength": 1}, + "name": {"type": "string"}, "geometry": {"type": "array", "minItems": 2, "items": {"$ref": "#/$defs/point"}}, + "walkable": {"type": "boolean"}, "provenance": {"$ref": "#/$defs/provenance"} + }, "additionalProperties": true + } + }, + "props": { + "type": "array", + "items": { + "type": "object", "required": ["id", "kind", "x", "y"], + "properties": {"id": {"type": "string"}, "kind": {"type": "string"}, "x": {"type": "number"}, "y": {"type": "number"}}, + "additionalProperties": true + } + }, + "extensions": {"type": "object"} + }, + "additionalProperties": false +} diff --git a/llms.txt b/llms.txt index 1e2e49d..3552871 100644 --- a/llms.txt +++ b/llms.txt @@ -11,6 +11,7 @@ - [anydoc](anydoc/SKILL.md): Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route to OCR tooling). - [api-design-and-evolution](api-design-and-evolution/SKILL.md): Design, document, review, and evolve consumer-facing APIs and event interfaces. Use when choosing REST/HTTP, GraphQL, RPC, events, webhooks, or streaming; writing OpenAPI or AsyncAPI contracts; defining schemas, pagination, mutations, errors, idempotency, or API compatibility; or planning API versioning, deprecation, and migration. Use secure-software-engineering for a full security lifecycle, ADR authoring for durable architecture decisions, and spec-driven-development for a delivery specification and implementation gates. - [artifact-pyramids](artifact-pyramids/SKILL.md): Organize durable agent research outputs as summaries, analysis, and evidence dossiers. Use when producing multi-layer research artifacts or coordinating research handoffs. +- [ascii-city-engine](ascii-city-engine/SKILL.md): Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-level interiors, general GIS analysis, or committing full-resolution GIS archives. - [autogen](autogen/SKILL.md): Expert skill for conversational multi-agent AI with Microsoft AutoGen. AssistantAgent, UserProxyAgent, GroupChat, code execution, nested chats, cancellation tokens, tool integration, and MCP support. Use when building conversation-driven multi-agent systems or comparing agent frameworks. - [backend-engineering](backend-engineering/SKILL.md): Design and implement backend services and APIs — REST, gRPC, GraphQL endpoint patterns, service architecture (clean/hexagonal/layered), database access patterns, integration and middleware design, error handling, and service-level testing. Language and framework agnostic. Do not use for frontend, data engineering, or platform infrastructure provisioning. - [binary-analysis](binary-analysis/SKILL.md): Analyze unknown binary files through a deterministic CLI that wraps Ghidra's static-analysis engine. Use when you need to inspect a PE, ELF, or Mach-O file — triage suspicious binaries, map imported APIs, decompile functions, trace call paths, or produce structured evidence reports. Do not use for runtime analysis (debugging, dynamic tracing, sandbox execution), for modifying or patching binaries, or for binaries you already know everything about. The skill owns planning, hypothesis formation, and evidence synthesis; the CLI owns all deterministic operations.