@nika-js/onlymap 0.5.12 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE.md +2 -0
  3. package/README.md +19 -7
  4. package/THIRD-PARTY-LICENSES.md +27 -0
  5. package/dist/{LercDecode.es-C97NsRLr.js → LercDecode.es-CJnypw8j.js} +1 -1
  6. package/dist/{basemap-kc0aQNqr.js → basemap-DrQ0-eyR.js} +1 -1
  7. package/dist/cityjson-D_V5GY8b.js +332 -0
  8. package/dist/crs-Ciu7Xs7a.js +108 -0
  9. package/dist/crs.d.ts +50 -0
  10. package/dist/ctx.d.ts +8 -1
  11. package/dist/elements/om-map.d.ts +26 -2
  12. package/dist/elements/om-overlay.d.ts +1 -1
  13. package/dist/elements/om-widget.d.ts +4 -0
  14. package/dist/feature-colors.d.ts +41 -0
  15. package/dist/{geoparquet-Dn1BypCS.js → geoparquet-Dix4lTNy.js} +1 -1
  16. package/dist/html-data.d.ts +1 -1
  17. package/dist/ifc-Dc-TdSJr.js +877 -0
  18. package/dist/ifc.d.ts +406 -0
  19. package/dist/{index-aBiHCzy-.js → index-1UgNlfGR.js} +20944 -18922
  20. package/dist/{index-BoUIm-jd.js → index-BQMjW5w0.js} +1 -1
  21. package/dist/{index-BcG3m2rF.js → index-B_1PPJgC.js} +1 -1
  22. package/dist/{index-DA_aop8u.js → index-CAuT5j9Y.js} +2 -2
  23. package/dist/{index-Dc-ZLLo8.js → index-CXPaeisL.js} +1 -1
  24. package/dist/index.d.ts +8 -0
  25. package/dist/layer-registry.d.ts +14 -0
  26. package/dist/layers/bim-layer.d.ts +104 -0
  27. package/dist/layers/feature-mesh-layer.d.ts +288 -0
  28. package/dist/layers/gltf-scene-walk.d.ts +55 -0
  29. package/dist/{lerc-74-SNPeU.js → lerc-CbTjQ7uI.js} +2 -2
  30. package/dist/license.d.ts +29 -5
  31. package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
  32. package/dist/onlymap.standalone.js +58057 -54751
  33. package/dist/onlymapjs.js +69 -67
  34. package/dist/programmatic.d.ts +22 -2
  35. package/dist/{raster-VyHCbPXy.js → raster-0b0nSHUh.js} +2 -2
  36. package/dist/{raster-pipeline-CGQTOUjw.js → raster-pipeline-D8siq7-4.js} +1 -1
  37. package/dist/react/om-overlay.d.ts +9 -0
  38. package/dist/react.js +144 -144
  39. package/dist/runtime-core.d.ts +110 -3
  40. package/dist/selection.d.ts +33 -0
  41. package/dist/site-placement.d.ts +85 -0
  42. package/dist/testing.d.ts +6 -2
  43. package/dist/tile3d-metadata.d.ts +185 -0
  44. package/dist/version.d.ts +1 -1
  45. package/dist/widget-registry.d.ts +11 -0
  46. package/dist/widgets/ifc-clash.d.ts +18 -0
  47. package/dist/widgets/ifc.d.ts +41 -0
  48. package/dist/{zarr-DddHNVim.js → zarr-DCYro_Vs.js} +79 -66
  49. package/docs/3d-assets.md +3 -3
  50. package/docs/live-data.md +2 -2
  51. package/docs/stories.md +1 -1
  52. package/docs/testing.md +2 -2
  53. package/llms.txt +3 -3
  54. package/onlymapjs.html-data.json +152 -0
  55. package/package.json +7 -2
  56. package/skills/onlymapjs/SKILL.md +4 -2
  57. package/skills/onlymapjs/references/react.md +1 -1
  58. package/skills/onlymapjs/references/syntax.md +299 -12
  59. package/dist/cityjson-urQeujQv.js +0 -407
package/CHANGELOG.md CHANGED
@@ -8,6 +8,25 @@ Note: npm collapsed a few closely-spaced releases — the GPX/FlatGeobuf (0.5.4)
8
8
  and GeoParquet (0.5.5) work shipped to npm together as **0.5.6**, so npm's
9
9
  version list jumps 0.5.3 → 0.5.6. Each logical version is listed here regardless.
10
10
 
11
+ ## 0.6.1 — unreleased
12
+
13
+ ### Added
14
+ - **`selection-type` on `<om-overlay>`** (React: `selectionType` on `<OmOverlay>`): scope a selection-anchored overlay to one pick type — `"click"` or `"hover"`. Fixes the reported ghost-popup bug: a popup wired to open on click re-anchored *and re-interpolated its template* on every hover pick (deck fires hover on all pointer movement), so it followed the pointer onto other features and rendered their objects into the wrong template with blank fields — with no author-side workaround. With `selection-type="click"`, hover picks are inert; a click on empty space still dismisses (empty picks now carry the pointer-event type internally), while hovering empty space no longer does. Absent attribute keeps the old behavior. Validation warns on a typoed value and on `selection-type` without `anchor-from="selection"`; the test harness's `clearSelection()` gains a kind (`"hover"` default, `"click"` for empty-space clicks).
15
+ - **BIM / IFC support**: `BIMLayer` loads `.ifc` files entirely in the browser (web-ifc WASM, CDN-fetched + integrity-pinned on first use, never bundled) and renders them as 3D Tiles; per-element **feature picking** on `Tile3DLayer` (`pick-features`, EXT_structural_metadata property tables, texture-backed IDs included); declarative **isolate / hide / ghost** (`feature-filter-field` + `isolate-features`/`hide-features`/`ghost-features`) and **style-by-property** (`feature-color-by`/`feature-color-scale`/`feature-palette`); **multi-model federation** (`ifc-loader federate`) and an AABB **clash-detection overlay**; widgets `ifc-loader`, `ifc-browser`, `feature-inspector` (alias `ifc-inspector`), `ifc-clash`; IFC georeferencing via `IfcMapConversion` (UTM + non-UTM projected CRS, grid-convergence heading correction) with a structured warning when a model's placement can't be trusted.
16
+
17
+ ### Changed
18
+ - **Explicit-terrain contract for georeferenced BIM**: the auto-terrain reaction — the library writing `terrain="mapterhorn"` onto the map at load time — was removed as a violation of the what-you-write-is-what-you-see contract. In its place, a model that actually resolves real-world elevation (`IfcMapConversion` + `OrthogonalHeight`) loading on a map with no `terrain` attribute raises a structured **error** through the validation channel (report-only, never a write; any authored value satisfies it, including an explicit `terrain="off"`). Checked at load time against the file's resolved facts, so flat models with no elevation data don't false-positive.
19
+ - Same contract for **basemap**: the `ifc-loader` widget no longer switches the map's `basemap` on/off as a side effect of a drop — `<om-map>` scene attributes are author-owned. A georeferenced model landing on a map with no basemap and no terrain raises a structured "no spatial context" warning instead.
20
+ - Widgets gained a `destroy` teardown hook; removing an `ifc-loader` (or its map) now releases all loaded-model resources.
21
+
22
+ ### Fixed
23
+ - **`ZarrLayer` ignored `select` changes**: writing the `select` attribute (the documented animation path) re-rendered stale cached chunks instead of re-slicing — deck's `TileLayer` only refetches when `updateTriggers.getTileData` changes, and that trigger was never wired. Scrubbing or animating any non-spatial dimension now repaints; the store stays open and cached across frames. Also guards a first-paint race on the same path: the frame drawn right after a re-slice could rasterize the reprojection mesh incompletely and stick (nothing scheduled another frame on an idle map) — one extra redraw is now poked in after the tiles load.
24
+
25
+ ## 0.6.0 — 2026-08-06
26
+
27
+ ### Changed
28
+ - **Licensing gate reworked: free-tier caps apply only on hosted http(s) pages.** In a dev context — localhost/loopback, `file://`, any non-web scheme, or headless — every cap (5 layers / 25k rows / 20 MB) lifts entirely. The attribution badge stays in all contexts. The exemption is a technical convenience, not a license grant: commercial deployment (hosted or packaged) still requires a commercial key — enforcement for shipped apps is legal, not technical (LICENSE.md §3).
29
+
11
30
  ## 0.5.12 — 2026-08-05
12
31
 
13
32
  ### Added
package/LICENSE.md CHANGED
@@ -35,6 +35,8 @@ The Software is commercial proprietary software.
35
35
 
36
36
  **Commercial use.** Any use other than Non-Commercial Use requires a purchased and maintained valid commercial license from Licensor, unless Licensor has granted Licensee a separate written license.
37
37
 
38
+ **Technical limits are a convenience, not the grant.** The Software may enforce technical limits in some execution contexts and not others (for example, limits may be inactive during local development, when loaded from local files, or inside application shells). The presence, absence, or failure of any technical limit does not grant, extend, or modify any license right. In particular, distributing the Software inside a packaged or installable application (desktop, mobile, or otherwise), or any other Commercial use, requires a valid commercial license under this Section 3 regardless of whether technical limits were active.
39
+
38
40
  Access to an npm package, repository, package tarball, source archive, or build artifact does not by itself grant any rights beyond the rights expressly granted in this Agreement.
39
41
 
40
42
  If an Order Form states usage limits, seat limits, application limits, domain limits, revenue limits, territory limits, term dates, or other license scope terms, Licensee must comply with those terms. If there is a conflict between this Agreement and an Order Form, the Order Form controls only for that conflict.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <om-widget type="legend" position="top-right"></om-widget>
27
27
 
28
- <om-overlay id="detail" anchor-from="selection" visible="false">
28
+ <om-overlay id="detail" anchor-from="selection" selection-type="click" visible="false">
29
29
  <div><b>{{place}}</b> — M {{magnitude}}</div>
30
30
  </om-overlay>
31
31
  <om-behavior on="click" layer="quakes" action="show-overlay" target="detail"></om-behavior>
@@ -80,9 +80,9 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
80
80
  | Element | Role |
81
81
  |---|---|
82
82
  | `<om-map>` | The map. `center`, `zoom`, `pitch`, `bearing`; `basemap` takes a free preset (`positron`, `liberty`, `dark-matter`, `osm`, …), a style URL, or `"none"` (standalone canvas) — and switches **live**; `validate` for a live on-page error panel. Give it a height: a custom element is `display:inline` by default, so the library injects a `display:block` default (fills a sized parent, else a 400px floor) to keep a bare map visible, but set an explicit height (`om-map { height: 100vh }`) for real layout — any height you set wins outright, including one below the floor. A map that still collapses warns in the console; `hidden` and `display:none` maps stay hidden and stay quiet. |
83
- | `<om-layer>` | Any of **36 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus `PopupLayer`, the native `COGLayer` for GeoTIFF rasters, `ZarrLayer` for chunked Zarr/GeoZarr rasters, and `ImageOverlay` for georeferenced drone JPEGs. `id` required; `label`/`color` feed the legend. |
84
- | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw`, `measure` (geodesic distance + area — live labels, `units` toggle, an `om-measure` readout event), `vega-lite` (live charts). Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
85
- | `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. `{{field}}` interpolates the picked feature, HTML-escaped by default. |
83
+ | `<om-layer>` | Any of **37 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus the built-in `PopupLayer` for WebGL badges/labels at scale, the native `COGLayer` for GeoTIFF rasters, `ZarrLayer` for chunked Zarr/GeoZarr rasters, `ImageOverlay` for georeferenced drone JPEGs, and `BIMLayer` for BIM source files (`.ifc` today) loaded straight in the browser, no pre-conversion step. `id` required; `label`/`color` feed the legend. |
84
+ | `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw`, `measure` (geodesic distance + area — live labels, `units` toggle, an `om-measure` readout event), `vega-lite` (live charts), and the BIM set `ifc-browser` / `feature-inspector` / `ifc-loader` / `ifc-clash`. Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
85
+ | `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. Selection-anchored overlays scope with `layer` (one layer's picks) and `selection-type="click"`/`"hover"` (one pick type — give a click-opened popup `selection-type="click"` so hovering elsewhere doesn't drag it along). `{{field}}` interpolates the picked feature, HTML-escaped by default. |
86
86
  | `<om-behavior>` | Declarative interactions: `on="click|hover|drag|load|data-loaded"` → a named action. |
87
87
  | `<om-story>` | A storyboard: `<om-step>` children fire actions on a timeline. Controlled by the `player` widget, behaviors, or `storyEl.play()/pause()/seek()`. |
88
88
  | `<om-fallback>` | What shows where scripts never run — chat-app/email file previews (iOS QuickLook renders HTML attachments with JS off), file managers, sandboxed webviews. A direct child of `<om-map>`, hidden automatically the moment the map boots; pages without one get a text-only default banner from the stylesheet. Good practice on any page that may travel as a file. The gate is pure CSS (`om-map:not(:defined)`), so `onlymapjs.css` must load without JS — a real `<link>`, a bundler-emitted sheet, or an inlined `<style>`. |
@@ -268,7 +268,19 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
268
268
 
269
269
  ## 3D
270
270
 
271
- `ScenegraphLayer` instances glTF/GLB models at coordinates (no three.js, no loader wiring), `Tile3DLayer` consumes OGC 3D Tiles, `GeoJsonLayer` extrudes polygons (`extruded get-elevation="$height"`), and the camera tilts via `pitch`/`bearing` attributes. Terrain is one attribute: `<om-map terrain="terrarium">` raises a real DEM surface (keyless AWS tiles; `maptiler-terrain` keyed; or a bring-your-own `{z}/{x}/{y}` DEM URL + `terrain-decoder`), geographic layers drape onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D models sit ON the surface), `terrain-exaggeration` scales the relief, and `terrain-texture` drapes imagery. Terrain replaces an active basemap while on (flat canvas vs raised surface) and restores it when off. Scene lighting is declarative: `<om-map lighting="daylight">` (presets `daylight`/`studio`/`flat`/`custom`, tuned via `lighting-ambient`, `lighting-sun`, `lighting-sun-azimuth`/`-elevation`, or `lighting-sun-date` for a solar-position sun) — attribute-backed, so lighting changes are undoable and story-steppable via the `set-lighting` action. `<om-widget type="lighting">` gives users native preset radios + tuning sliders over the same attributes. Converting IFC/CAD upstream: [docs/3d-assets.md](docs/3d-assets.md).
271
+ `ScenegraphLayer` instances glTF/GLB models at coordinates (no three.js, no loader wiring), `Tile3DLayer` consumes OGC 3D Tiles, `GeoJsonLayer` extrudes polygons (`extruded get-elevation="$height"`), and the camera tilts via `pitch`/`bearing` attributes. Terrain is one attribute: `<om-map terrain="terrarium">` raises a real DEM surface (keyless AWS tiles; `mapterhorn` keyless too, paired with a CARTO Positron drape by default; `maptiler-terrain` keyed; or a bring-your-own `{z}/{x}/{y}` DEM URL + `terrain-decoder`), geographic layers drape onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D models sit ON the surface), `terrain-exaggeration` scales the relief, and `terrain-texture` drapes imagery. Terrain replaces an active basemap while on (flat canvas vs raised surface) and restores it when off. **BIM models require explicit terrain**: a georeferenced model's absolute elevation only means anything against a DEM surface, and the library never writes attributes for you — a model that resolves real elevation on a map without a `terrain` attribute raises an error through the validation channel at load time (any authored value satisfies it, including an explicit `terrain="off"` for flat-ground siting). Scene lighting is declarative: `<om-map lighting="daylight">` (presets `daylight`/`studio`/`flat`/`custom`, tuned via `lighting-ambient`, `lighting-sun`, `lighting-sun-azimuth`/`-elevation`, or `lighting-sun-date` for a solar-position sun) — attribute-backed, so lighting changes are undoable and story-steppable via the `set-lighting` action. `<om-widget type="lighting">` gives users native preset radios + tuning sliders over the same attributes. Converting IFC/CAD upstream: [docs/3d-assets.md](docs/3d-assets.md).
272
+
273
+ **Per-element picking on 3D Tiles (BIM/photogrammetry).** A `pickable` `Tile3DLayer` picks a whole tile; adding `pick-features` picks an individual ELEMENT — a wall, a window, one IFC product — and the `selection` carries that element's `featureId`, `properties` and `class`, resolved from the tile's own `EXT_mesh_features` + `EXT_structural_metadata` (`feature-id-property` selects the ID set, default `_FEATURE_ID_0`). Isolate/hide/ghost are declarative and mirror the vector filter vocabulary — `feature-filter-field="component"` names the metadata field, then `isolate-features`/`hide-features`/`ghost-features` take JSON value lists (`ghost-opacity` tunes the fade); hiding is a shader discard, so a hidden element also stops being pickable and you can select what's behind it, and `isolate-features` is exclusive (anything unlisted is hidden). Because they're attributes they're undoable and story-steppable. Underneath, `feature-styles` recolours, fades or highlights elements by feature ID — an array indexed by id of `{color, strength, opacity}` — uploaded as a small lookup texture, so restyling costs no refetch and no re-tesselation. IDs stored per texel (how photogrammetry classification ships) are sampled per FRAGMENT, so element boundaries are pixel-sharp rather than snapped to triangle edges. Multi-material models fan out correctly (glTF allows one material per primitive, so a five-material house is five primitives and a real IFC export is often dozens); only genuinely instanced i3dm content falls back to tile-granularity picking. Two limits, stated plainly: texture-backed IDs need `load-options` with `gltf.loadImages`/`loadBuffers` **and** `image: {"type": "data"}` (without it loaders.gl reads the whole ID texture back through a canvas once per vertex and the tileset takes minutes to appear); and `opacity` below 1 currently blanks the model, so use `strength` to tint for now. See `dev/examples/ferry-building-features.html`.
274
+
275
+
276
+ **The BIM widgets.** Three built-ins turn a `pick-features` tileset into a usable model browser without page JS. `ifc-browser` (renamed from `ifc-legend`, which still works — it browses a model, it doesn't explain symbology) groups the model by any property-table field (`fields="ifcClass material container spatialPath"`), counts each value, and gives every row I/H/G buttons that write `isolate-features`/`hide-features`/`ghost-features` — so the panel is a UI over the attributes, and everything it does is undoable and story-steppable; it keeps the companion outline layer's `filter-categories` in step too, since hidden elements would otherwise leave their edges behind. It also offers whichever **model trees** the file supports — spatial (IfcSite → IfcBuilding → IfcStorey), type, system, and classification — each expandable, counts aggregated upward, the same I/H/G on every node so isolating a storey, a duct system or a CCS code reaches everything beneath it. Spatial isn't privileged: on a real Danish project the classification tree covered 3,415 elements to spatial's 660. A tree isn't a second mode or a second widget — it's a group-by on a metadata column, which is why it reuses the visibility attributes untouched (and why there's one browser per layer: those attributes are single-valued). `loadIfc` extracts every hierarchy a model has and emits a column only when the file populates it, so a tree that would render empty is never offered. `feature-inspector` (renamed from `ifc-inspector`, which still works — its body is generic property-row rendering with no IFC dependency, so it works unchanged on a non-IFC `pick-features` layer) shows the picked element's properties (`fields="ifcClass material container netVolume"`). `ifc-loader` is a drop zone that parses an `.ifc` in the browser and builds both layers for it; add `federate` and one drop zone accepts SEVERAL models into a co-registered scene, each with its own visibility toggle — the shape coordination tools use, because a real project has architecture, structure, mechanical, electrical and plumbing rather than two. `ifc-clash` compares any two loaded models. Colour is a question, not decoration: `feature-color-by` colours categorically by a field, `feature-color-scale` graduates over a numeric one, `feature-palette` and `feature-color-strength` tune them — and setting *neither* is the default, leaving the model in its own IFC surface colours. Widget scripts reach the same data through `ctx.features(layerId)` (the decoded property table) plus the `features` watch token, which fires when the table decodes off the first tile.
277
+
278
+ **Clash detection, coarse and honest.** `<om-widget type="ifc-clash">` flags element pairs from two co-registered models whose bounding boxes interpenetrate, colours both sides, and flies to the centre of each overlap. Results are grouped by element the way coordination tools report them — one wall crossing four ducts is one row, not four — with a highlight switch and an isolation mode (`Dim Other` / `Hide Other`, Navisworks' own terms) so the pair you selected is the only thing that reads. On a real architecture-vs-HVAC pair it finds 1,443 clashes across 9.6M candidate combinations in ~96 ms, entirely in the browser.
279
+
280
+ It is deliberately **not** Navisworks. An axis-aligned box test over-reports anything diagonal, and it cannot tell a resolved penetration from a collision: on that same pair, 676 of the results are air diffusers sitting inside ceilings, which is how buildings work. Treat the number as *"here are the places two disciplines occupy the same box — go look"*, not as a defect list. Persisting and sharing results is what BCF exists for and is not attempted here.
281
+
282
+ **In-browser IFC.** `loadIfc(bytes)` parses an `.ifc` with web-ifc (WASM) and returns a 3D Tiles model held entirely in memory — nothing uploaded, nothing written to disk. Because the output *is* a tileset, per-element picking, styling, `site-*` and isolate/hide/ghost work on it unchanged. It returns `tilesetUrl`/`edgesUrl` blob URLs, the property table, the georeferencing (`lonLat`, `heading`, `scale`, and a `headingSource` that distinguishes a real reading from an assumption), per-phase `timings`, and a `revoke()` you must call when swapping models. web-ifc is MPL-2.0 and is *not* a package dependency — it's dynamic-imported from unpkg on first use, so it never reaches the bundle and pages that never open an IFC pay nothing (measured: the IFC chunk is 8.5 KB gzipped and contains only the URL). **For offline, air-gapped or strict-CSP deployments**, `npm run vendor:web-ifc public/vendor/web-ifc` copies the four files you serve yourself, then `wasm-path="/vendor/web-ifc/"` on the loader — 1.37 MB gzipped of *static assets*, with the JS bundle unchanged. Note that web-ifc is only one network dependency: a georeferenced model also switches a basemap on, so an offline page wants `basemap="none"` and `telemetry="off"` too. See `dev/examples/bim-workbench.html`.
283
+ **Georeferencing is declarative too.** `site-origin="[lng, lat]"` (optionally `[lng, lat, elevation]`), `site-heading` (a bearing, degrees clockwise from true north) and `site-scale` place a model from the markup instead of baking a position into the tileset — `site-origin` overrides whatever the root transform carries, and rotation/scale pivot on the model's own anchor so a heading change spins the building about itself. They work on `Tile3DLayer` and on `PathLayer` (whose paths are then local east/north/up metres), which is what lets an IFC mesh and its outline overlay move together. This matters more than it sounds: authoring tools ship a default project location and a default is indistinguishable from a survey — the buildingSMART Medical-Dental Clinic sample carries Revit's Boston default, the Duplex a Chicago city-centre point — while `IfcMapConversion` is absent from most IFC2x3 exports and `TrueNorth` is routinely unset. Being attributes, corrections are undoable and story-steppable rather than a reconversion. Whenever a model resolves anything short of a real `IfcMapConversion` — `IfcSite` coordinates or no georeference at all — `BIMLayer` and `ifc-loader` raise a structured `"warning"` through the same validation channel other `om-layer` errors use (the `validate` on-page panel, `om-validation-error`'s `detail.warnings`), rather than only a status-line sentence a human has to notice; it never fails the map (`valid` stays `true`), it just names the layer and points at `site-origin`/`site-heading` as the fix. See `dev/examples/bim-sample.html`.
272
284
 
273
285
  ## Programmatic surface
274
286
 
@@ -281,7 +293,7 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
281
293
 
282
294
  ## Free tier & licensing
283
295
 
284
- **Non-commercial use is free with attribution** (LICENSE.md §3); commercial use requires a license. Without a license key, maps run on the **free plan**: up to **5 layers** and **25,000 rows per layer** (20 MB per data fetch), with a small "OnlyMap by NIKA. Free for non-commercial use." badge in the corner. Exceeding a limit never breaks the map. Past the **layer** cap the extra layer simply doesn't render; past the **row** cap the layer renders its first 25,000 rows (an arbitrary subset, in source order) rather than nothing, with a dismissible on-map notice saying the data is partial — so a capped map is still useful to look at without ever pretending it's complete. Either way the validation stream/error panel tells you exactly which limit, why, and how to lift it. Limits apply identically everywhere (including localhostno dev/prod surprises).
296
+ **Non-commercial use is free with attribution** (LICENSE.md §3); commercial use requires a license. Without a license key, maps run on the **free plan**: up to **5 layers** and **25,000 rows per layer** (20 MB per data fetch), with a small "OnlyMap by NIKA. Free for non-commercial use." badge in the corner. Exceeding a limit never breaks the map. Past the **layer** cap the extra layer simply doesn't render; past the **row** cap the layer renders its first 25,000 rows (an arbitrary subset, in source order) rather than nothing, with a dismissible on-map notice saying the data is partial — so a capped map is still useful to look at without ever pretending it's complete. Either way the validation stream/error panel tells you exactly which limit, why, and how to lift it. **Limits apply only on hosted http(s) pages**in a dev context (localhost, `file://`, or any non-web scheme) every cap lifts so you can build without friction; the attribution badge stays in all contexts. The exemption is a technical convenience, not a license grant: commercial deployment — hosted **or** shipped inside a packaged app — requires a commercial key regardless of whether the technical gates fired (LICENSE.md §3).
285
297
 
286
298
  A license key lifts all limits and removes the badge:
287
299
 
@@ -300,7 +312,7 @@ The library reports one **deployment-scoped** usage snapshot per map per page lo
300
312
 
301
313
  ## Not implemented yet (honestly)
302
314
 
303
- Mapbox GL basemaps, depth-interleaved 3D compositing, globe projection, SSE transport, multi-field filters, `dblclick` behaviors, the `transform` data pipeline, the typed fluent builder, and stories/draw as React components (both work via the HTML manifest).
315
+ Mapbox GL basemaps, depth-interleaved 3D compositing, globe projection, SSE transport, multi-field filters, `dblclick` behaviors, the `transform` data pipeline, the typed fluent builder, and stories/draw as React components (both work via the HTML manifest). On `pick-features`: instanced (i3dm) tiles keep tile-granularity picking, and `feature-styles`' `opacity` below 1 is still being validated. On BIM: clash detection is bounding-box only (no mesh-level test, and it cannot tell a resolved penetration from a collision), there is no BCF export, no equivalent of Navisworks' Auto Reveal, and prepared tilesets converted before the `bbox*` columns existed cannot take part in a clash pass — `strength` tinting and `opacity: 0` (hide, a shader discard) work.
304
316
 
305
317
  ## Going deeper
306
318
 
@@ -168,6 +168,7 @@ hand.
168
168
  | long | 3.2.0 | Apache-2.0 |
169
169
  | maplibre-gl | 5.24.0 | BSD-3-Clause |
170
170
  | md5 | 2.3.0 | BSD-3-Clause |
171
+ | meshoptimizer | 1.2.0 | MIT |
171
172
  | mgrs | 1.0.0 | MIT |
172
173
  | minimist | 1.2.8 | MIT |
173
174
  | mjolnir.js | 3.0.0 | MIT |
@@ -7178,6 +7179,32 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
7178
7179
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
7179
7180
  ```
7180
7181
 
7182
+ ## meshoptimizer@1.2.0 (MIT)
7183
+
7184
+ ```
7185
+ MIT License
7186
+
7187
+ Copyright (c) 2016-2026 Arseny Kapoulkine
7188
+
7189
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7190
+ of this software and associated documentation files (the "Software"), to deal
7191
+ in the Software without restriction, including without limitation the rights
7192
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7193
+ copies of the Software, and to permit persons to whom the Software is
7194
+ furnished to do so, subject to the following conditions:
7195
+
7196
+ The above copyright notice and this permission notice shall be included in all
7197
+ copies or substantial portions of the Software.
7198
+
7199
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
7200
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7201
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
7202
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
7203
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
7204
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7205
+ SOFTWARE.
7206
+ ```
7207
+
7181
7208
  ## mgrs@1.0.0 (MIT)
7182
7209
 
7183
7210
  ```
@@ -19,7 +19,7 @@ async function v(N = {}) {
19
19
  if (n) {
20
20
  const { createRequire: I } = await import(
21
21
  /*webpackIgnore:true*/
22
- "./lerc-74-SNPeU.js"
22
+ "./lerc-CbTjQ7uI.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }
@@ -1,4 +1,4 @@
1
- import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-aBiHCzy-.js";
1
+ import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-1UgNlfGR.js";
2
2
  const Cd = Math.PI / 180, gy = 180 / Math.PI;
3
3
  function em(Pe, Q = 0) {
4
4
  const me = Math.min(180, Pe) * Cd;
@@ -0,0 +1,332 @@
1
+ import { C as F, a as q } from "./crs-Ciu7Xs7a.js";
2
+ const j = 0.5;
3
+ function G(e, t) {
4
+ if (!Array.isArray(e)) return [];
5
+ const n = t?.scale?.[0] ?? 1, o = t?.scale?.[1] ?? 1, s = t?.scale?.[2] ?? 1, a = t?.translate?.[0] ?? 0, i = t?.translate?.[1] ?? 0, f = t?.translate?.[2] ?? 0, r = new Array(e.length);
6
+ for (let c = 0; c < e.length; c++) {
7
+ const u = e[c];
8
+ r[c] = [u[0] * n + a, u[1] * o + i, (u[2] ?? 0) * s + f];
9
+ }
10
+ return r;
11
+ }
12
+ const T = {
13
+ MultiPoint: 1,
14
+ MultiLineString: 2,
15
+ MultiSurface: 3,
16
+ CompositeSurface: 3,
17
+ Solid: 4,
18
+ MultiSolid: 5,
19
+ CompositeSolid: 5
20
+ };
21
+ function J(e, t) {
22
+ let n = e;
23
+ for (let o = 0; o < t; o++) n = n.flat();
24
+ return n;
25
+ }
26
+ function N(e) {
27
+ const t = T[e.type ?? ""];
28
+ return !t || t < 3 || !Array.isArray(e.boundaries) ? [] : J(e.boundaries, t - 3);
29
+ }
30
+ function X(e, t) {
31
+ const n = new Array(t).fill(void 0), o = T[e.type ?? ""], s = e.semantics?.surfaces, a = e.semantics?.values;
32
+ if (!o || !Array.isArray(s) || !Array.isArray(a)) return n;
33
+ const i = J(a, o - 3);
34
+ for (let f = 0; f < t; f++) {
35
+ const r = i[f];
36
+ typeof r == "number" && (n[f] = s[r]?.type);
37
+ }
38
+ return n;
39
+ }
40
+ function _(e) {
41
+ const t = typeof e == "number" ? e : Number.parseFloat(String(e ?? ""));
42
+ return Number.isFinite(t) ? t : -1;
43
+ }
44
+ function K(e, t) {
45
+ if (!Array.isArray(e)) return;
46
+ const n = e.filter((o) => N(o).length > 0);
47
+ if (n.length !== 0) {
48
+ if (t !== void 0) {
49
+ const o = n.find((s) => _(s.lod) === t);
50
+ if (o) return o;
51
+ }
52
+ return n.reduce((o, s) => _(s.lod) > _(o.lod) ? s : o);
53
+ }
54
+ }
55
+ const Q = /EPSG.*?(\d{4,6})\D*$/i;
56
+ function tt(e) {
57
+ if (!e) return;
58
+ const t = Number(Q.exec(e)?.[1]);
59
+ return Number.isFinite(t) ? t : void 0;
60
+ }
61
+ let P = !1;
62
+ async function U(e, t) {
63
+ const n = { toLngLat: (r, c) => [r, c], projected: !1 }, o = tt(e);
64
+ if (o === void 0)
65
+ return P || (P = !0, console.warn(
66
+ `[onlymapjs] data="${t}": CityJSON has no metadata.referenceSystem — assuming coordinates are already WGS84 lon/lat. Add a referenceSystem to the file if they are not.`
67
+ )), n;
68
+ const s = q[o] ?? o, a = F[s];
69
+ if (!a)
70
+ throw new Error(
71
+ `CityJSON declares EPSG:${o}${s === o ? "" : ` (horizontal component EPSG:${s})`}, which is not one of the bundled coordinate systems (${Object.keys(F).join(", ")}). Reproject the file to EPSG:4326 server-side (cjio --reproject 4326), or claim the URL with OmMap.registerFormat() and supply your own projection.`
72
+ );
73
+ if (a.geographic)
74
+ return a.swapXY ? { toLngLat: (r, c) => [c, r], projected: !1 } : n;
75
+ const i = (await import("./index-CW1n5LdO.js")).default, f = i(a.def, "+proj=longlat +datum=WGS84 +no_defs");
76
+ return {
77
+ toLngLat: (r, c) => {
78
+ const [u, p] = f.forward(a.swapXY ? [c, r] : [r, c]);
79
+ return [u, p];
80
+ },
81
+ projected: !0
82
+ };
83
+ }
84
+ function I(e) {
85
+ let t = 0, n = 0, o = 0;
86
+ for (let s = 0; s < e.length; s++) {
87
+ const a = e[s], i = e[(s + 1) % e.length];
88
+ t += (a[1] - i[1]) * (a[2] + i[2]), n += (a[2] - i[2]) * (a[0] + i[0]), o += (a[0] - i[0]) * (a[1] + i[1]);
89
+ }
90
+ return Math.hypot(t, n, o) / 2;
91
+ }
92
+ function et(e) {
93
+ const t = e.slice().sort((o, s) => o - s), n = t.length >> 1;
94
+ return t.length % 2 ? t[n] : (t[n - 1] + t[n]) / 2;
95
+ }
96
+ function B(e, t, n, o, s) {
97
+ const a = K(t.geometry, s);
98
+ if (!a) return;
99
+ const i = N(a);
100
+ if (i.length === 0) return;
101
+ const f = X(a, i.length), r = i.map(
102
+ (d) => d.map((y) => y.map((l) => n[l]).filter((l) => l !== void 0))
103
+ );
104
+ let c = 1 / 0, u = -1 / 0;
105
+ for (const d of r)
106
+ for (const y of d)
107
+ for (const [, , l] of y)
108
+ l < c && (c = l), l > u && (u = l);
109
+ if (!Number.isFinite(c)) return;
110
+ const p = c, g = f.some((d) => d === "RoofSurface"), h = [];
111
+ let S = 1 / 0, m = -1 / 0;
112
+ for (let d = 0; d < r.length; d++)
113
+ if (!(g && f[d] !== "RoofSurface"))
114
+ for (const y of r[d])
115
+ for (const [, , l] of y)
116
+ (g || l > p + j) && (h.push(l), l < S && (S = l), l > m && (m = l));
117
+ h.length === 0 && (h.push(u), S = u, m = u);
118
+ let w = f.indexOf("GroundSurface");
119
+ if (w < 0) {
120
+ let d = -1;
121
+ for (let y = 0; y < r.length; y++) {
122
+ const l = r[y][0];
123
+ if (!l || l.length < 3) continue;
124
+ const b = l.every(([, , R]) => R <= p + j), C = I(l) * (b ? 1e3 : 1);
125
+ C > d && (d = C, w = y);
126
+ }
127
+ }
128
+ if (w < 0) return;
129
+ const O = r[w];
130
+ if (!O[0] || O[0].length < 3) return;
131
+ const [, k] = o.toLngLat(O[0][0][0], O[0][0][1]), z = 111320 * Math.cos(k * Math.PI / 180), Y = 110540, V = (d) => o.projected ? d : d.map(([y, l, b]) => {
132
+ const [C, R] = o.toLngLat(y, l);
133
+ return [C * z, R * Y, b];
134
+ });
135
+ let L = 0, E = 0;
136
+ for (let d = 0; d < r.length; d++) {
137
+ if (g && f[d] !== "RoofSurface") continue;
138
+ const y = r[d][0];
139
+ if (!y || y.length < 3) continue;
140
+ const l = I(V(y));
141
+ L += l, E += l * (y.reduce((b, [, , C]) => b + C, 0) / y.length);
142
+ }
143
+ const W = L > 0 ? E / L : et(h), H = {
144
+ cityobject_id: e,
145
+ cityobject_type: t.type ?? "Unknown",
146
+ ...t.parents?.[0] !== void 0 ? { parent_id: t.parents[0] } : {},
147
+ ...a.lod !== void 0 ? { lod: String(a.lod) } : {},
148
+ ground_height: v(p),
149
+ roof_height: v(W - p),
150
+ eaves_height: v(S - p),
151
+ ridge_height: v(m - p),
152
+ roof_area: v(L),
153
+ surface_count: i.length
154
+ };
155
+ return { faceRings: r, semantics: f, footprintIndex: w, derived: H };
156
+ }
157
+ function nt(e, t, n, o, s, a) {
158
+ const i = B(e, t, n, o, a);
159
+ if (!i) return;
160
+ const r = i.faceRings[i.footprintIndex].filter((c) => c.length >= 3).map((c) => {
161
+ const u = c.map(([p, g]) => o.toLngLat(p, g));
162
+ return u.push(u[0]), u;
163
+ });
164
+ return {
165
+ type: "Feature",
166
+ id: e,
167
+ geometry: { type: "Polygon", coordinates: r },
168
+ // Source attributes first: the derived names are the ones manifests bind
169
+ // to, so they must be deterministic even against a same-named source field.
170
+ properties: { ...s, ...t.attributes, ...i.derived }
171
+ };
172
+ }
173
+ const M = {
174
+ GroundSurface: "#999999",
175
+ WallSurface: "#ffffff",
176
+ RoofSurface: "#ff0000",
177
+ TrafficArea: "#6e6e6e",
178
+ AuxiliaryTrafficArea: "#2c8200",
179
+ Window: "#0059ff",
180
+ Door: "#640000"
181
+ }, x = {
182
+ Building: "#7497df",
183
+ BuildingPart: "#7497df",
184
+ BuildingInstallation: "#7497df",
185
+ Bridge: "#999999",
186
+ BridgePart: "#999999",
187
+ BridgeInstallation: "#999999",
188
+ BridgeConstructiveElement: "#999999",
189
+ CityObjectGroup: "#ffffb3",
190
+ LandUse: "#ffffb3",
191
+ CityFurniture: "#cc0000",
192
+ GenericCityObject: "#cc0000",
193
+ PlantCover: "#39ac39",
194
+ SolitaryVegetationObject: "#39ac39",
195
+ Railway: "#000000",
196
+ Road: "#999999",
197
+ TransportSquare: "#999999",
198
+ TINRelief: "#ffdb99",
199
+ Tunnel: "#999999",
200
+ TunnelPart: "#999999",
201
+ TunnelInstallation: "#999999",
202
+ WaterBody: "#4da6ff"
203
+ }, ot = "#b0b0b0";
204
+ function rt(e, t) {
205
+ return e && M[e] ? M[e] : t && x[t] ? x[t] : ot;
206
+ }
207
+ function it(e, t, n, o, s, a) {
208
+ const i = B(e, t, n, o, a);
209
+ if (!i) return [];
210
+ const f = [];
211
+ for (let r = 0; r < i.faceRings.length; r++) {
212
+ const c = i.faceRings[r], u = c[0];
213
+ if (!u || u.length < 3) continue;
214
+ const p = c.filter((h) => h.length >= 3).map((h) => h.map(([S, m, w]) => [...o.toLngLat(S, m), w])), g = p[0];
215
+ f.push({
216
+ ...s,
217
+ ...t.attributes,
218
+ ...i.derived,
219
+ surface_type: i.semantics[r],
220
+ fill_color: rt(i.semantics[r], t.type),
221
+ polygon: p,
222
+ outline: [...g, g[0]]
223
+ });
224
+ }
225
+ return f;
226
+ }
227
+ function v(e) {
228
+ return Number.isFinite(e) ? Math.round(e * 1e3) / 1e3 : 0;
229
+ }
230
+ function D(e, t, n, o, s = "footprint") {
231
+ if (!e) return [];
232
+ const a = (f) => Array.isArray(f.children) && f.children.some((r) => {
233
+ const c = e[r];
234
+ return c?.type === `${f.type}Part` && (c.geometry?.some((u) => N(u).length > 0) ?? !1);
235
+ }), i = [];
236
+ for (const [f, r] of Object.entries(e)) {
237
+ if (!r || typeof r != "object" || a(r)) continue;
238
+ const u = (r.parents?.[0] ? e[r.parents[0]] : void 0)?.attributes ?? {};
239
+ if (s === "surfaces")
240
+ i.push(...it(f, r, t, n, u, o));
241
+ else {
242
+ const p = nt(f, r, t, n, u, o);
243
+ p && i.push(p);
244
+ }
245
+ }
246
+ return i;
247
+ }
248
+ function A(e, t, n) {
249
+ if (e?.type !== n)
250
+ throw new Error(
251
+ `expected a ${n} object but found type="${e?.type ?? "undefined"}" — this does not look like CityJSON. If it is a different newline-delimited format, claim the URL with OmMap.registerFormat().`
252
+ );
253
+ }
254
+ function $(e) {
255
+ const t = /[?&]om-lod=([^&#]+)/.exec(e)?.[1];
256
+ if (t === void 0) return;
257
+ const n = Number.parseFloat(decodeURIComponent(t));
258
+ if (!Number.isFinite(n)) {
259
+ console.warn(`[onlymapjs] om-lod="${t}" is not a number — ignoring and using the highest available LoD.`);
260
+ return;
261
+ }
262
+ return n;
263
+ }
264
+ function Z(e) {
265
+ const t = /[?&]om-surfaces=([^&#]+)/.exec(e)?.[1];
266
+ if (t === void 0) return !1;
267
+ const n = decodeURIComponent(t).toLowerCase();
268
+ return n === "1" || n === "true";
269
+ }
270
+ async function ut(e, t) {
271
+ const n = await e.json();
272
+ A(n, t, "CityJSON");
273
+ const o = await U(n.metadata?.referenceSystem, t), s = G(n.vertices, n.transform), a = Z(t) ? "surfaces" : "footprint";
274
+ return D(n.CityObjects, s, o, $(t), a);
275
+ }
276
+ const st = 250, at = 512;
277
+ async function* ct(e) {
278
+ if (!e.body) {
279
+ for (const s of (await e.text()).split(`
280
+ `)) yield s;
281
+ return;
282
+ }
283
+ const t = e.body.getReader(), n = new TextDecoder();
284
+ let o = "";
285
+ for (; ; ) {
286
+ const { done: s, value: a } = await t.read();
287
+ if (s) break;
288
+ o += n.decode(a, { stream: !0 });
289
+ let i;
290
+ for (; (i = o.indexOf(`
291
+ `)) >= 0; )
292
+ yield o.slice(0, i), o = o.slice(i + 1);
293
+ }
294
+ o += n.decode(), o.length > 0 && (yield o);
295
+ }
296
+ async function lt(e, t, n) {
297
+ const o = $(t), s = Z(t) ? "surfaces" : "footprint";
298
+ let a, i, f = !1;
299
+ const r = [];
300
+ let c = 0, u = 0;
301
+ for await (const p of ct(e)) {
302
+ const g = p.trim();
303
+ if (g.length === 0) continue;
304
+ const h = JSON.parse(g);
305
+ if (f)
306
+ A(h, t, "CityJSONFeature");
307
+ else if (A(h, t, "CityJSON"), i = h.transform, a = await U(h.metadata?.referenceSystem, t), f = !0, h.CityObjects && Object.keys(h.CityObjects).length === 0) continue;
308
+ const S = G(h.vertices, i);
309
+ r.push(...D(h.CityObjects, S, a, o, s)), c++;
310
+ const m = performance.now();
311
+ (c >= at || m - u >= st) && (c = 0, u = m, n(r.slice()));
312
+ }
313
+ if (!f)
314
+ throw new Error(
315
+ 'CityJSONSeq file is empty or has no header line — the first line must be a CityJSON object carrying "transform" and "metadata", followed by one CityJSONFeature per line.'
316
+ );
317
+ return r;
318
+ }
319
+ export {
320
+ K as chooseGeometry,
321
+ D as decodeCityObjects,
322
+ G as decompressVertices,
323
+ rt as defaultFillColor,
324
+ X as faceSemantics,
325
+ N as flattenToFaces,
326
+ ut as parseCityJsonDocument,
327
+ lt as parseCityJsonSeq,
328
+ tt as parseEpsgCode,
329
+ $ as parseLodPin,
330
+ Z as parseSurfacesFlag,
331
+ U as resolveCrs
332
+ };
@@ -0,0 +1,108 @@
1
+ const c = {
2
+ 7415: 28992,
3
+ // Amersfoort / RD New + NAP height — the 3DBAG default
4
+ 6697: 6668,
5
+ // JGD2011 geographic 3D + height — the PLATEAU default
6
+ 5555: 25832,
7
+ // ETRS89 / UTM 32N + DHHN92 height
8
+ 5556: 25833
9
+ // ETRS89 / UTM 33N + DHHN92 height
10
+ }, f = [
11
+ [33, 129.5],
12
+ [33, 131],
13
+ [36, 132.166666666667],
14
+ [33, 133.5],
15
+ [36, 134.333333333333],
16
+ [36, 136],
17
+ [36, 137.166666666667],
18
+ [36, 138.5],
19
+ [36, 139.833333333333],
20
+ [40, 140.833333333333],
21
+ [44, 140.25],
22
+ [44, 142.25],
23
+ [44, 144.25],
24
+ [26, 142],
25
+ [26, 127.5],
26
+ [26, 124],
27
+ [26, 131],
28
+ [20, 136],
29
+ [26, 154]
30
+ ], u = {
31
+ // Netherlands — Amersfoort / RD New (3DBAG).
32
+ 28992: {
33
+ def: "+proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.4171,50.3319,465.5524,-0.398957,0.343988,-1.8774,4.0725 +units=m +no_defs"
34
+ },
35
+ // Switzerland — CH1903+ / LV95.
36
+ 2056: {
37
+ def: "+proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs"
38
+ },
39
+ // Germany — ETRS89 / UTM zones 32N and 33N.
40
+ 25832: { def: "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs" },
41
+ 25833: { def: "+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs" },
42
+ // Plain WGS84 UTM zones 32N/33N (EPSG:326xx) — the generic, non-European-specific
43
+ // code many CityJSON files declare instead of the ETRS89 pair above (e.g.
44
+ // Ingolstadt's own open-data LoD3 export). Numerically near-identical to
45
+ // 25832/25833 at map-rendering precision (WGS84 vs. ETRS89 differ by ~cm in
46
+ // Europe), so no +towgs84 shift is needed — same bare projection string.
47
+ 32632: { def: "+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs" },
48
+ 32633: { def: "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs" },
49
+ // Austria — MGI / Austria GK West/Central/East (the national cadastral
50
+ // system's three meridian-strip zones; Vienna sits in GK East). Same
51
+ // Bessel ellipsoid + towgs84 shift and y_0=-5,000,000 false-northing
52
+ // convention across all three — proj4 strings verified against epsg.io,
53
+ // not hand-derived, and cross-checked by reprojecting a real Vienna
54
+ // open-data file's own extent and confirming it lands in Vienna.
55
+ 31254: { def: "+proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
56
+ 31255: { def: "+proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
57
+ 31256: { def: "+proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
58
+ // Singapore — SVY21 / Singapore TM (the national grid; NUS's own Singapore
59
+ // HDB buildings dataset declares this via metadata.referenceSystem
60
+ // directly, unlike the Austria/3DBAG files, which had none). proj4 string
61
+ // verified against epsg.io, cross-checked by reprojecting a real dataset's
62
+ // own extent and confirming it lands in Singapore (103.68-103.99°E,
63
+ // 1.27-1.46°N — matches).
64
+ 3414: { def: "+proj=tmerc +lat_0=1.36666666666667 +lon_0=103.833333333333 +k=1 +x_0=28001.642 +y_0=38744.572 +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs" },
65
+ // Japan — JGD2011 geographic 2D. EPSG axis order is (lat, lon).
66
+ 6668: { def: "+proj=longlat +ellps=GRS80 +no_defs", geographic: !0, swapXY: !0 },
67
+ // WGS 84. Files in the wild store lon/lat despite EPSG's formal (lat, lon).
68
+ 4326: { def: "+proj=longlat +datum=WGS84 +no_defs", geographic: !0 },
69
+ 3857: { def: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +no_defs" }
70
+ };
71
+ for (let t = 0; t < f.length; t++) {
72
+ const [e, o] = f[t];
73
+ u[6669 + t] = {
74
+ def: `+proj=tmerc +lat_0=${e} +lon_0=${o} +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs`,
75
+ swapXY: !0
76
+ };
77
+ }
78
+ let p = null;
79
+ async function m() {
80
+ return (await import("./index-CW1n5LdO.js")).default;
81
+ }
82
+ function g() {
83
+ return p ??= m(), p;
84
+ }
85
+ function j() {
86
+ return Object.keys(u).map(Number).sort((t, e) => t - e);
87
+ }
88
+ async function i(t, e, o) {
89
+ const a = c[t] ?? t, n = u[a];
90
+ if (!n) return null;
91
+ const [s, l] = n.swapXY ? [o, e] : [e, o];
92
+ if (n.geographic) return [s, l];
93
+ const _ = await g(), [r, d] = _(n.def, "+proj=longlat +datum=WGS84 +no_defs").forward([s, l]);
94
+ return Number.isFinite(r) && Number.isFinite(d) ? [r, d] : null;
95
+ }
96
+ async function w(t, e, o, a = 100) {
97
+ const n = await i(t, e, o), s = await i(t, e, o + a);
98
+ if (!n || !s) return null;
99
+ const l = (n[1] + s[1]) / 2 * Math.PI / 180, _ = (s[0] - n[0]) * Math.cos(l), r = s[1] - n[1];
100
+ return Math.atan2(_, r) * 180 / Math.PI;
101
+ }
102
+ export {
103
+ u as C,
104
+ c as a,
105
+ j as b,
106
+ w as g,
107
+ i as p
108
+ };