@nika-js/onlymap 0.6.0 → 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.
- package/CHANGELOG.md +15 -1
- package/README.md +18 -6
- package/THIRD-PARTY-LICENSES.md +27 -0
- package/dist/{LercDecode.es-fbo6h5rt.js → LercDecode.es-CJnypw8j.js} +1 -1
- package/dist/{basemap-CprSan9q.js → basemap-DrQ0-eyR.js} +1 -1
- package/dist/cityjson-D_V5GY8b.js +332 -0
- package/dist/crs-Ciu7Xs7a.js +108 -0
- package/dist/crs.d.ts +50 -0
- package/dist/ctx.d.ts +8 -1
- package/dist/elements/om-map.d.ts +26 -2
- package/dist/elements/om-overlay.d.ts +1 -1
- package/dist/elements/om-widget.d.ts +4 -0
- package/dist/feature-colors.d.ts +41 -0
- package/dist/{geoparquet-Bm8pfxyo.js → geoparquet-Dix4lTNy.js} +1 -1
- package/dist/html-data.d.ts +1 -1
- package/dist/ifc-Dc-TdSJr.js +877 -0
- package/dist/ifc.d.ts +406 -0
- package/dist/{index-Bz-rbmhj.js → index-1UgNlfGR.js} +20892 -18886
- package/dist/{index-GOwMjqm6.js → index-BQMjW5w0.js} +1 -1
- package/dist/{index-DSRIZR81.js → index-B_1PPJgC.js} +1 -1
- package/dist/{index-CCQttJzO.js → index-CAuT5j9Y.js} +2 -2
- package/dist/{index-UzmNhuS0.js → index-CXPaeisL.js} +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/layer-registry.d.ts +14 -0
- package/dist/layers/bim-layer.d.ts +104 -0
- package/dist/layers/feature-mesh-layer.d.ts +288 -0
- package/dist/layers/gltf-scene-walk.d.ts +55 -0
- package/dist/{lerc-CuifOkoA.js → lerc-CbTjQ7uI.js} +2 -2
- package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
- package/dist/onlymap.standalone.js +57996 -54706
- package/dist/onlymapjs.js +69 -67
- package/dist/programmatic.d.ts +22 -2
- package/dist/{raster-dysYIQ_k.js → raster-0b0nSHUh.js} +2 -2
- package/dist/{raster-pipeline-ZdCOGfqV.js → raster-pipeline-D8siq7-4.js} +1 -1
- package/dist/react/om-overlay.d.ts +9 -0
- package/dist/react.js +144 -144
- package/dist/runtime-core.d.ts +110 -3
- package/dist/selection.d.ts +33 -0
- package/dist/site-placement.d.ts +85 -0
- package/dist/testing.d.ts +6 -2
- package/dist/tile3d-metadata.d.ts +185 -0
- package/dist/version.d.ts +1 -1
- package/dist/widget-registry.d.ts +11 -0
- package/dist/widgets/ifc-clash.d.ts +18 -0
- package/dist/widgets/ifc.d.ts +41 -0
- package/dist/{zarr-OewK7k2K.js → zarr-DCYro_Vs.js} +79 -66
- package/docs/testing.md +2 -2
- package/llms.txt +3 -3
- package/onlymapjs.html-data.json +152 -0
- package/package.json +3 -1
- package/skills/onlymapjs/SKILL.md +4 -2
- package/skills/onlymapjs/references/react.md +1 -1
- package/skills/onlymapjs/references/syntax.md +298 -11
- package/dist/cityjson-urQeujQv.js +0 -407
|
@@ -16,8 +16,8 @@ Vite/npm project:
|
|
|
16
16
|
Static CDN page (raw-file CDNs only — unpkg/jsDelivr; never esm.sh or another rebundling CDN, which duplicates the WebGL runtime and breaks layer shaders):
|
|
17
17
|
|
|
18
18
|
```html
|
|
19
|
-
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.
|
|
20
|
-
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.6.
|
|
19
|
+
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.1/dist/onlymapjs.css">
|
|
20
|
+
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.6.1"></script>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Always include `onlymapjs.css` — it carries the MapLibre basemap styles and the no-JS fallback rules (`<om-fallback>` / default banner). For the fallback to work in script-disabled previews it must load without JavaScript: a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages (a bundler-emitted stylesheet is fine in npm projects).
|
|
@@ -42,7 +42,7 @@ Common attributes:
|
|
|
42
42
|
- `map-id="<random UUID>"` — identifies the map artifact for usage telemetry (not a visitor id). Include one on new complete pages, freshly generated — never copied from an example; deleting it is the opt-out.
|
|
43
43
|
- `telemetry="off"` disables usage telemetry for this map (global: `OmMap.configureTelemetry({ disabled: true })`)
|
|
44
44
|
- `license-key="om_live_…"` — lifts the free-plan limits (5 layers, 25k rows per layer — a layer past the layer cap doesn't render, a layer past the row cap renders its first 25k rows plus a dismissible on-map notice; caps apply only on HOSTED http(s) pages — localhost/file:// and other dev contexts run uncapped, badge stays) and removes the corner badge; publishable origin-restricted token, safe in page source (or `OmMap.configureLicense(key)` once). Free-plan violations don't break the map: the offending layer isn't rendered and validation names the limit.
|
|
45
|
-
- `terrain="terrarium|<preset>|<{z}/{x}/{y} DEM URL>|off"` — 3D elevation surface. `terrarium` is keyless (AWS); `maptiler-terrain` needs `basemap-key`/`configureBasemap`; raw DEM URLs need `terrain-decoder` (`terrarium`, `mapbox-rgb`, or `{rScaler,gScaler,bScaler,offset}` JSON). `terrain-exaggeration` scales relief (1 = true); `terrain-max-zoom` = the provider's REAL tileset cap; `terrain-texture` drapes a `{z}/{x}/{y}` imagery template. Geographic layers drape automatically; per-layer `terrain="drape|offset|off"` overrides (3D-model layers default to `offset`). Terrain REPLACES an active basemap while on (restored when off) — validation warns. Register presets with `OmMap.registerTerrain(name, {...})`; `set-terrain` action + `terrain` watch token; attribute-backed (undoable).
|
|
45
|
+
- `terrain="terrarium|<preset>|<{z}/{x}/{y} DEM URL>|off"` — 3D elevation surface. `terrarium` is keyless (AWS); `mapterhorn` is also keyless and carries a CARTO Positron drape by default; `maptiler-terrain` needs `basemap-key`/`configureBasemap`; raw DEM URLs need `terrain-decoder` (`terrarium`, `mapbox-rgb`, or `{rScaler,gScaler,bScaler,offset}` JSON). `terrain-exaggeration` scales relief (1 = true); `terrain-max-zoom` = the provider's REAL tileset cap; `terrain-texture` drapes a `{z}/{x}/{y}` imagery template. Geographic layers drape automatically; per-layer `terrain="drape|offset|off"` overrides (3D-model layers default to `offset`). Terrain REPLACES an active basemap while on (restored when off) — validation warns. Register presets with `OmMap.registerTerrain(name, {...})`; `set-terrain` action + `terrain` watch token; attribute-backed (undoable). **BIM requires explicit terrain**: a model that resolves real elevation on a map with no `terrain` attribute raises an ERROR through the validation channel at load time — the library never writes `terrain` for you; author a preset (`mapterhorn` pairs a keyless DEM with a CARTO drape) or an explicit `terrain="off"` for flat-ground siting.
|
|
46
46
|
- `lighting="daylight|studio|flat|custom"` — scene lighting for 3D content (extruded polygons, models); absent = deck defaults. Preset seeds values; `lighting-ambient`, `lighting-sun` (intensity; 0 removes the sun), `lighting-sun-azimuth` (° CW from north), `lighting-sun-elevation` (° above horizon), `lighting-camera` (model-inspection fill) override individual fields; `lighting-sun-date` (ISO 8601 or epoch ms) computes the sun from solar position at the map center and wins over azimuth/elevation. Attribute-backed: changes are undoable, and the `set-lighting {lighting, sunAzimuth, …}` action makes lighting story-steppable (`lighting="default"` removes the whole attribute set; a bare preset is a clean reset). `<om-widget type="lighting">` is the native UI. Widget scripts can `watch = ["lighting"]`.
|
|
47
47
|
- `widgets-dim="off"` disables collision-dim — by default a widget slot dims (`--om-widget-opacity-dimmed`, 0.35) while an open `<om-overlay>` popup covers it, rather than the popup dodging (attribution/toggle slots never dim).
|
|
48
48
|
- `headless width="800" height="600"` for test harness use
|
|
@@ -137,7 +137,7 @@ For an epoch-millisecond filter, format the built-in widget's numeric labels dec
|
|
|
137
137
|
|
|
138
138
|
Use the `type` value exactly:
|
|
139
139
|
|
|
140
|
-
`A5Layer`, `ArcLayer`, `BitmapLayer`, `COGLayer`, `ColumnLayer`, `ContourLayer`, `GeoJsonLayer`, `GeohashLayer`, `GreatCircleLayer`, `GridCellLayer`, `GridLayer`, `H3ClusterLayer`, `H3HexagonLayer`, `HeatmapLayer`, `HexagonLayer`, `IconLayer`, `ImageOverlay`, `LineLayer`, `MVTLayer`, `PathLayer`, `PointCloudLayer`, `PolygonLayer`, `PopupLayer`, `QuadkeyLayer`, `S2Layer`, `ScatterplotLayer`, `ScenegraphLayer`, `ScreenGridLayer`, `SimpleMeshLayer`, `SolidPolygonLayer`, `TerrainLayer`, `TextLayer`, `Tile3DLayer`, `TileLayer`, `TripsLayer`, `ZarrLayer`.
|
|
140
|
+
`A5Layer`, `ArcLayer`, `BIMLayer`, `BitmapLayer`, `COGLayer`, `ColumnLayer`, `ContourLayer`, `GeoJsonLayer`, `GeohashLayer`, `GreatCircleLayer`, `GridCellLayer`, `GridLayer`, `H3ClusterLayer`, `H3HexagonLayer`, `HeatmapLayer`, `HexagonLayer`, `IconLayer`, `ImageOverlay`, `LineLayer`, `MVTLayer`, `PathLayer`, `PointCloudLayer`, `PolygonLayer`, `PopupLayer`, `QuadkeyLayer`, `S2Layer`, `ScatterplotLayer`, `ScenegraphLayer`, `ScreenGridLayer`, `SimpleMeshLayer`, `SolidPolygonLayer`, `TerrainLayer`, `TextLayer`, `Tile3DLayer`, `TileLayer`, `TripsLayer`, `ZarrLayer`.
|
|
141
141
|
|
|
142
142
|
Common choices:
|
|
143
143
|
|
|
@@ -147,6 +147,7 @@ Common choices:
|
|
|
147
147
|
- Aggregation: `HeatmapLayer`, `HexagonLayer`, `GridLayer`, `ScreenGridLayer`.
|
|
148
148
|
- Tiles: `TileLayer`, `MVTLayer`, `Tile3DLayer`.
|
|
149
149
|
- 3D models: `ScenegraphLayer`, `SimpleMeshLayer`, `PointCloudLayer`, `Tile3DLayer`.
|
|
150
|
+
- BIM source files (`.ifc`, loaded in-browser, no pre-conversion step): `BIMLayer`.
|
|
150
151
|
- GeoTIFF/COG rasters: `COGLayer`.
|
|
151
152
|
- Zarr / GeoZarr rasters (chunked N-D arrays): `ZarrLayer`.
|
|
152
153
|
- Geotagged drone JPEGs: `ImageOverlay`.
|
|
@@ -359,6 +360,14 @@ Built-ins:
|
|
|
359
360
|
- `lighting` — scene-lighting controller: preset radios (Off/daylight/studio/flat/custom) + ambient/sun/azimuth/elevation/camera sliders, all over the lighting* attributes via `set-lighting` (undoable; re-syncs when anything else writes them). A bare preset click is a clean RESET (stale lighting-* overrides removed); a slider edit flips to `custom` and sets only the touched key.
|
|
360
361
|
- `widgets-toggle` — one button hiding/showing all OTHER widgets (`widgets-hidden` attribute / `set-widgets-visible {visible}` action; bare payload toggles). Hidden = visibility, never removal — widget state survives; attribution and the toggle itself never hide. Transient (not an undo step), story-scrub-capturable.
|
|
361
362
|
- `undo-redo` — undo/redo buttons over the manifest history (layer toggles, filters, basemap switches, element edits, drawn sketches). Keyboard works without the widget: Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, Ctrl-Y. Camera moves, hover effects, and story playback are not undo steps.
|
|
363
|
+
- `ifc-browser` — the model browser (registered as `ifc-legend` too, the deprecated original name): group the model by any property-table field, colour it, and isolate/hide/ghost per value. `layer` names the `pick-features` layer (defaults to the only one on the map), `fields="ifcClass material container"` are the group-by choices, `scale-fields="netVolume"` adds graduated numeric ramps, `rows="9"` caps the visible list before it scrolls. Each row has 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. **I is multi-select**: pressing it on several rows (or several tree nodes) isolates their union, and pressing it again on one removes just that one, so "ground floor AND roof" is a normal thing to ask for. In a tree, ancestors of an isolated node stay legible so the branch is still reachable from the root. It also keeps the companion outline layer's `filter-categories` in step, or hidden elements would leave their edges behind. Colour mode defaults to the model's own IFC surface colours; `no-color` removes the select. Non-physical classes (`IfcSpace`, `IfcOpeningElement`, …) are hidden from the list unless `show-non-physical` is set, because they otherwise dominate the counts with things nobody can see.
|
|
364
|
+
|
|
365
|
+
The select additionally offers whichever **trees** the file supports: **Spatial** (`spatialPath` — IfcSite → IfcBuilding → IfcStorey → space), **Type** (`typePath` — IfcDoorType → Single-Flush), **System** (`systemPath` — a distribution system and its parent systems) and **Classification** (`classificationPath` — CCS → [L]BB Fundamentskonstruktion, built by walking `ReferencedSource` rather than parsing the code string, which would only work for whichever scheme's punctuation you guessed). Each renders expandable, counts aggregated upward, with the same I/H/G on every node — isolating a storey, a system or a classification code reaches every element beneath it. Spatial is not privileged: on a real Danish project the classification tree covered 3,415 elements against spatial's 660.
|
|
366
|
+
|
|
367
|
+
A tree is **not** a separate mode — it groups by a hierarchy column exactly as the list groups by `ifcClass`, so it reuses the reset-on-field-change rule, the colour wiring and the isolate/hide/ghost attributes untouched, and a fifth tree would cost one column plus one line. `loadIfc` extracts every hierarchy a model has and emits a column **only when the file populates it** (all four measured at 26 ms of a 290 ms relationship pass on 2,626 elements, and lost in the noise on a 74 MB one), so a tree that would render empty is never offered and an authored hierarchy field in `fields` is dropped rather than shown dead. Trees appear automatically when available — you do not have to list them — while `field="spatialPath"` opens straight onto one. Tilesets converted before these columns existed carry none. Elements with no container get their own bucket rather than disappearing. **One browser per layer:** `feature-filter-field` and the isolate/hide/ghost attributes are single-valued, so two instances pointed at one layer would clobber each other's filtering.
|
|
368
|
+
- `ifc-clash` — clash overlay over two CO-REGISTERED model layers. With no `layers` attribute it offers two SELECTS over whatever models are loaded, which is how coordination tools work (append models, then choose a pair) and the only shape that survives a project with five disciplines; `layers="arch mep"` still pins a fixed pair. Controls follow Navisworks' Clash Detective, which is what a coordinator already knows: a **highlight-all** switch in the header, and an **isolation MODE** above the list (`none` / `dim others` / `hide others`) rather than independent toggles — two switches would offer four states, two of them meaningless. Isolation applies to the SELECTED clash, so with nothing selected the model is untouched. Hiding is `opacity: 0`, a shader discard, not partial transparency. Sides are red and BLUE rather than Navisworks' conventional red/green, which is the worst possible pair for a red-green deficiency. CLICKING A ROW focuses that clash: the chosen pair goes full strength, every other clashing element drops to a faint tint, and the camera flies to the centre of the overlap. Without focus, painting all 527 clashing elements the same red means flying to one shows you a red building. Results are GROUPED by the element on side A — one wall crossing four ducts is one row with a count, not four, which is how coordination tools report and what keeps a four-figure result readable. Starts COLLAPSED with the count in its header — a real pass returns four figures of rows and a permanently open list buries the model. Attributes: `tolerance` in metres of real interpenetration before a pair counts (0 reports any overlap), `rows`, `zoom`. Flags element pairs whose bounding boxes interpenetrate, colours both sides through `feature-styles` (which outranks `feature-color-by`, so clearing it hands the colouring back), and gives each pair a Z button that flies to the centre of the OVERLAP rather than of either element. v1 is an axis-aligned box test — it finds a duct through a roof in milliseconds with no server, and it over-reports anything diagonal, since a brace's box is far bigger than the brace. Zones, spaces, openings and proxies are excluded (a zone is a volume, so a box test says it intersects everything inside it), as are pairs sharing a class and name, which are reference markers repeated across disciplines. Persisting or sharing results is what BCF exists for and is not attempted. **Co-registration is checked, not assumed**: differing `site-origin` values are reported rather than silently returning zero, because two mis-registered models look exactly like two clean ones.
|
|
369
|
+
- `feature-inspector` (renamed from `ifc-inspector`, which still works as an alias — the widget's body is generic property-row rendering with no IFC dependency, so it works unchanged on any `pick-features` layer, IFC-derived or not) — properties of the currently picked element. `fields="ifcClass material container netVolume"` chooses the rows, `placeholder` is the nothing-selected text. Reads the same property table picking resolves, so it needs no data of its own.
|
|
370
|
+
- `ifc-loader` — a drop zone that parses an `.ifc` in the browser and builds the layers for it (see **In-browser IFC** below). Add `federate` and ONE drop zone accepts several models into a co-registered scene — one layer per model, each with its own visibility toggle and remove button — instead of dedicating a widget per discipline. Under `federate`, the FIRST model loaded decides the shared model-space origin and placement, and every later model inherits both (discipline exports of one building routinely disagree by kilometres, so honouring each file's own would scatter it); add `independent` to opt out when the models are unrelated buildings rather than disciplines of one. That sharing never applies without `federate` — a plain loader always resolves each new file's own georeference — and it resets once every model in a federated scene is removed, so the next one dropped in starts fresh rather than inheriting a dead scene's position. `layer="ifc"` is the id it creates (plus `<id>-edges`), `zoom` the flyTo zoom, `field` the filter field, `outline-color`/`no-outlines`/`ghost-opacity` tune what it builds, and `site-origin`/`site-heading`/`site-scale` override what the file declares. If a loaded model turns out to be georeferenced it is AUTO-PLACED: the widget writes the file's own coordinates, heading and scale onto the layers it CREATED and flies the camera there — but it NEVER touches `<om-map>`'s own scene attributes (`basemap`, `terrain`): those are author-owned, and a georeferenced model landing on a map with neither raises a structured warning ("no spatial context") instead of switching one on.
|
|
362
371
|
|
|
363
372
|
Positions — 8 managed slots (logical, RTL-aware): `top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`. Legacy corner names (`top-left`, `top-right`, `bottom-left`, `bottom-right`) are aliases. Same-slot widgets stack in one library-owned flex container: flush edges, shared gap — never overlapping. `order="1"` sets deterministic in-slot ordering (default: DOM order). Adjacent COMPACT button widgets (zoom-controls, undo-redo, widgets-toggle) in one slot auto-merge into a single control group (shared radius/shadow, 1px dividers); `cluster="false"` keeps one out — validation warns if set on a non-compact widget. At map widths ≤640px, managed widgets auto-fold into top/end/bottom/start disclosure drawers; `fold="never"` keeps an essential control outside, `widgets-fold="off"` opts the map out, and `--om-widget-fold-breakpoint` changes the map-width threshold. `position="manual"` opts out of management: the widget renders as a plain block you place with your own CSS (even outside the map, e.g. in an app header, driving the map through actions). Layout tokens: `--om-widget-inset-x/-y` (slot inset, default 12px), `--om-widget-gap-x/-y` (stack gap, default 8px), `--om-widget-opacity`, `--om-widget-opacity-dimmed` (default 0.35 — the collision-dim level), `--om-widget-radius`, `--om-widget-fold-breakpoint` — or the no-CSS sugar attribute `<om-map widget-style="gap:10 opacity:0.9 inset:16">` (keys: inset, gap, inset-x/-y, gap-x/-y, opacity, radius, size; numbers are px except opacity).
|
|
364
373
|
|
|
@@ -416,12 +425,17 @@ watched token fires:
|
|
|
416
425
|
</om-widget>
|
|
417
426
|
```
|
|
418
427
|
|
|
419
|
-
- `ctx.layers
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
-
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
- `ctx.layers`
|
|
429
|
+
- `ctx.data(id)`
|
|
430
|
+
- `ctx.dataInViewport(id)`
|
|
431
|
+
- `ctx.stats(id, field, { scope: "viewport" })`
|
|
432
|
+
- `ctx.selection`
|
|
433
|
+
- `ctx.viewport`
|
|
434
|
+
- `ctx.features(layerId)` — the decoded `EXT_structural_metadata` property table of a `pick-features` tileset, one row per element, or `undefined` before the first tile carrying one has landed. This is what a BIM panel groups and counts by; pair it with the `features` watch token, since the table cannot exist on the first render.
|
|
435
|
+
- `ctx.history` — `{ canUndo, canRedo }`; re-render on changes via the `history` watch token
|
|
436
|
+
- `ctx.emit(action, payload)`
|
|
437
|
+
|
|
438
|
+
Watch tokens: `data:<layerId>`, `viewport`, `selection`, `layers` (fires on layer add/remove, visibility, and filter changes), `features` (fires when a tileset's property table decodes), `basemap`, `lighting`, `terrain`, `history`, `widgets` (fires on a `widgets-hidden` hide-all toggle). An EMPTY `watch` array means the widget never re-renders — omit it or list tokens, never `watch: []`.
|
|
425
439
|
|
|
426
440
|
**Drive the map — the emission contract.** This is the part authors get wrong.
|
|
427
441
|
A widget NEVER mutates the map directly and NEVER dispatches its own
|
|
@@ -481,6 +495,11 @@ Anchors:
|
|
|
481
495
|
- `anchor-from="selection"`
|
|
482
496
|
- `anchor-layer="regions" anchor-feature-id="mission"`
|
|
483
497
|
|
|
498
|
+
Selection scoping (both only apply with `anchor-from="selection"`):
|
|
499
|
+
|
|
500
|
+
- `layer="quakes"` — only that layer's picks move/re-template the overlay.
|
|
501
|
+
- `selection-type="click"` (or `"hover"`) — only that pick type does. A click-opened popup NEEDS `selection-type="click"`: without it, merely hovering any pickable feature drags the popup there and re-interpolates its template against the hovered object (wrong-layer ghost popup). With it, hover is inert and a click on empty space still dismisses. `"hover"` is the mirror for hover-driven overlays.
|
|
502
|
+
|
|
484
503
|
Templates:
|
|
485
504
|
|
|
486
505
|
- `{{field}}` HTML-escaped interpolation.
|
|
@@ -489,7 +508,7 @@ Templates:
|
|
|
489
508
|
Example:
|
|
490
509
|
|
|
491
510
|
```html
|
|
492
|
-
<om-overlay id="detail" anchor-from="selection" visible="false">
|
|
511
|
+
<om-overlay id="detail" anchor-from="selection" selection-type="click" visible="false">
|
|
493
512
|
<div><b>{{place}}</b> M {{magnitude}}</div>
|
|
494
513
|
</om-overlay>
|
|
495
514
|
<om-behavior on="click" layer="quakes" action="show-overlay" target="detail"></om-behavior>
|
|
@@ -635,3 +654,271 @@ For 3D Tiles LOD/refinement experiments, use:
|
|
|
635
654
|
maximum-memory-usage="256"
|
|
636
655
|
view-distance-scale="0.85"></om-layer>
|
|
637
656
|
```
|
|
657
|
+
|
|
658
|
+
### Per-element (BIM) picking and styling
|
|
659
|
+
|
|
660
|
+
A plain `pickable` `Tile3DLayer` picks a whole TILE. To pick individual
|
|
661
|
+
elements — a wall, a window, one IFC product — add `pick-features`. The pick's
|
|
662
|
+
`selection` then carries `featureId`, `properties` and `class` resolved from the
|
|
663
|
+
tile's own `EXT_mesh_features` + `EXT_structural_metadata`:
|
|
664
|
+
|
|
665
|
+
```html
|
|
666
|
+
<om-layer id="building" type="Tile3DLayer"
|
|
667
|
+
tileset="https://example.com/tileset.json"
|
|
668
|
+
pick-features
|
|
669
|
+
feature-id-property="_FEATURE_ID_0"
|
|
670
|
+
load-options='{"gltf":{"loadBuffers":true,"loadImages":true},"image":{"type":"data"}}'
|
|
671
|
+
pickable></om-layer>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
`feature-styles` recolours, fades or highlights elements by feature ID — an
|
|
675
|
+
array indexed BY id, each entry `{color: [r,g,b], strength: 0-1, opacity: 0-1}`.
|
|
676
|
+
Set it live (from a widget, a dropdown, a pick handler) and only a small lookup
|
|
677
|
+
texture is re-uploaded; no refetch, no re-tesselation:
|
|
678
|
+
|
|
679
|
+
```html
|
|
680
|
+
<om-layer id="building" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
681
|
+
feature-styles='[{"color":[90,200,255],"strength":0.5},{},{"color":[255,215,130],"strength":0.9}]'></om-layer>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Use `strength` below 1 to TINT rather than replace — a full-strength colour hides
|
|
685
|
+
the model's own texture entirely.
|
|
686
|
+
|
|
687
|
+
**Isolate / hide / ghost** are declarative, and mirror the vector
|
|
688
|
+
`filter-field` + `filter-categories` pair: name the metadata field once, then
|
|
689
|
+
list values per state. Values are JSON arrays, matched against the tile's own
|
|
690
|
+
property table:
|
|
691
|
+
|
|
692
|
+
```html
|
|
693
|
+
<om-layer id="building" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
694
|
+
feature-filter-field="component"
|
|
695
|
+
hide-features='["Windows","Skylight"]'
|
|
696
|
+
ghost-features='["Wall"]'
|
|
697
|
+
ghost-opacity="0.18"></om-layer>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
- `isolate-features` is exclusive — anything NOT listed is hidden, so it is a
|
|
701
|
+
scope rather than another kind of hide.
|
|
702
|
+
- Hiding is a shader `discard`, so a hidden element also stops being pickable
|
|
703
|
+
and you can select whatever sits behind it.
|
|
704
|
+
- These compose ONTO `feature-styles` rather than replacing it: the style table
|
|
705
|
+
supplies colour, these supply visibility. Changing colour scheme never
|
|
706
|
+
un-hides anything, and isolating never drops your colouring.
|
|
707
|
+
- Being attributes, they are undoable and story-steppable — prefer them over
|
|
708
|
+
computing a `feature-styles` table in page JS.
|
|
709
|
+
|
|
710
|
+
**Colour by property.** `feature-styles` is indexed by feature ID, which means
|
|
711
|
+
computing a table by hand. These name a property-table FIELD instead and build
|
|
712
|
+
that table for you, once the table arrives with the first tile:
|
|
713
|
+
|
|
714
|
+
```html
|
|
715
|
+
<om-layer id="clinic" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
716
|
+
feature-color-by="material"></om-layer>
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
- `feature-color-by` is CATEGORICAL — one palette entry per distinct value.
|
|
720
|
+
`feature-palette='["#4f7cff","#ff9a3c"]'` overrides the built-in
|
|
721
|
+
colour-blind-safe cycle.
|
|
722
|
+
- `feature-color-scale` is GRADUATED over a numeric field.
|
|
723
|
+
- `feature-color-strength` (default 0.85) is how hard the colour is mixed over
|
|
724
|
+
the model's own material; below 1 tints rather than replaces.
|
|
725
|
+
- Setting NEITHER is meaningful, and is the default: the model renders in its
|
|
726
|
+
own IFC surface colours, which is what someone opening a building expects.
|
|
727
|
+
Reach for these to answer a question, not to make it look coloured.
|
|
728
|
+
- An authored `feature-styles` always wins — these are sugar over the same
|
|
729
|
+
table, never an override of it.
|
|
730
|
+
|
|
731
|
+
A graduated ramp needs the field to actually be populated. Revit IFC2x3 exports
|
|
732
|
+
frequently carry no `IfcElementQuantity` at all, so every `netVolume` is 0 and
|
|
733
|
+
the ramp renders flat — check the property table before blaming the ramp.
|
|
734
|
+
|
|
735
|
+
**Georeferencing — `site-origin` / `site-heading` / `site-scale`** (Tile3DLayer
|
|
736
|
+
and PathLayer). Where a model sits is a viewing decision, not a conversion one,
|
|
737
|
+
so it lives on the layer:
|
|
738
|
+
|
|
739
|
+
```html
|
|
740
|
+
<om-layer id="clinic" type="Tile3DLayer" tileset="…/tileset.json" pick-features pickable
|
|
741
|
+
site-origin="[-71.059776, 42.358429]" site-heading="32" site-scale="1"></om-layer>
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- `site-origin` is `[lng, lat]` or `[lng, lat, elevation]`, and it OVERRIDES the
|
|
745
|
+
position baked into the tileset's root transform rather than offsetting it.
|
|
746
|
+
- `site-heading` is a bearing — degrees CLOCKWISE from true north. On its own,
|
|
747
|
+
with no `site-origin`, it rotates the model where it stands.
|
|
748
|
+
- Rotation and scale pivot on the model's own anchor, not the tileset origin,
|
|
749
|
+
so a heading change spins the building about itself.
|
|
750
|
+
- An IFC model is a PAIR of layers — the mesh tileset and a `PathLayer` outline
|
|
751
|
+
overlay whose paths are local east/north/up metres — and both need the same
|
|
752
|
+
three values, or the building walks away from its own edges. The
|
|
753
|
+
`ifc-loader` widget sets them on both for you.
|
|
754
|
+
- Do not trust a model's declared position without looking at it. Authoring
|
|
755
|
+
tools ship a default project location, and a default is indistinguishable
|
|
756
|
+
from a survey: the buildingSMART Medical-Dental Clinic sample carries Revit's
|
|
757
|
+
Boston default and the Duplex a Chicago city-centre point, so both land on
|
|
758
|
+
occupied downtown blocks at an arbitrary rotation. `IfcMapConversion` (real
|
|
759
|
+
georeferencing) is absent from most IFC2x3 exports and `TrueNorth` is
|
|
760
|
+
routinely unset — which is exactly what these attributes are for.
|
|
761
|
+
- Editing `site-*` on a live Tile3DLayer reloads the tileset (deck.gl only
|
|
762
|
+
reloads on a URL change, so the runtime cache-busts the URL). The outline
|
|
763
|
+
PathLayer updates as a uniform, with no reload.
|
|
764
|
+
|
|
765
|
+
Three constraints worth knowing before promising this to a user:
|
|
766
|
+
|
|
767
|
+
- Multi-material models are fine: glTF allows one material per primitive, so a
|
|
768
|
+
five-material house is five primitives and a real IFC export is often dozens —
|
|
769
|
+
all of them get per-element picking. Only genuinely instanced (i3dm) tiles fall
|
|
770
|
+
back to tile-granularity picking, where `feature-styles` does nothing.
|
|
771
|
+
- Datasets that store IDs in a TEXTURE (photogrammetry classification) need
|
|
772
|
+
`load-options` with `gltf.loadImages`, `gltf.loadBuffers` AND
|
|
773
|
+
`image: {"type": "data"}`. Without the last one the tileset takes minutes to
|
|
774
|
+
appear — loaders.gl otherwise reads the whole ID texture back through a canvas
|
|
775
|
+
once per vertex.
|
|
776
|
+
- `opacity: 0` HIDES an element (a shader discard, so it also stops being
|
|
777
|
+
pickable and you can select what is behind it). Partial `opacity` (ghosting) is
|
|
778
|
+
still being validated.
|
|
779
|
+
|
|
780
|
+
Worked example: `examples/ferry-building-features.html`.
|
|
781
|
+
|
|
782
|
+
### BIMLayer — declarative in-browser loading
|
|
783
|
+
|
|
784
|
+
`<om-layer type="BIMLayer" src="./model.ifc">` is the declarative counterpart
|
|
785
|
+
to `ifc-loader`/`loadIfc`: point it at a BIM source file (an `.ifc` today,
|
|
786
|
+
other formats plug into the same layer later) and it runs the file through
|
|
787
|
+
the loader itself the moment `src` resolves — no pre-baked tileset, no
|
|
788
|
+
`site-origin`/`site-heading`/`site-scale` (the file's own georeference is
|
|
789
|
+
read and applied automatically; the attributes exist to OVERRIDE a wrong or
|
|
790
|
+
missing reading, not to restate what the loader already computed — an
|
|
791
|
+
authored `site-origin` on a BIMLayer is not wired up yet, a documented gap),
|
|
792
|
+
and no separate `<om-layer type="PathLayer">` for the outline overlay (it's
|
|
793
|
+
added automatically). Everything else about a picked BIM layer — `pick-features`
|
|
794
|
+
(defaults ON, unlike a plain `Tile3DLayer`), `feature-filter-field`,
|
|
795
|
+
`feature-styles`, `isolate-features`/`hide-features`/`ghost-features`,
|
|
796
|
+
`feature-color-by`, `ghost-opacity` — works exactly as it does on
|
|
797
|
+
`Tile3DLayer`, because BIMLayer forwards them to a real `Tile3DLayer` it
|
|
798
|
+
builds internally:
|
|
799
|
+
|
|
800
|
+
```html
|
|
801
|
+
<om-layer id="clinic" type="BIMLayer" src="./clinic.ifc"
|
|
802
|
+
label="Medical-Dental Clinic"
|
|
803
|
+
feature-filter-field="ifcClass"
|
|
804
|
+
ghost-opacity="0.15"
|
|
805
|
+
pickable></om-layer>
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Reach for `BIMLayer` when the model is fixed and known ahead of time
|
|
809
|
+
(`dev/examples/bim-sample.html`); reach for the `ifc-loader` widget (BIM
|
|
810
|
+
workbench) when a visitor picks the file, or when several models need to
|
|
811
|
+
federate into one coordinated scene. **Known gap:** the outline overlay does
|
|
812
|
+
not yet follow isolate/hide/ghost the way the mesh does — `ifc-browser`'s
|
|
813
|
+
visibility sync targets a separate `<layer-id>-edges` element by convention,
|
|
814
|
+
and BIMLayer's outline never reaches the DOM as one.
|
|
815
|
+
|
|
816
|
+
### In-browser IFC
|
|
817
|
+
|
|
818
|
+
`loadIfc` parses an `.ifc` with web-ifc (WASM) and returns a 3D Tiles model held
|
|
819
|
+
entirely in memory — nothing is uploaded and nothing is written to disk. Because
|
|
820
|
+
the output IS a tileset, `pick-features`, `feature-styles`, `site-*` and the
|
|
821
|
+
declarative isolate/hide/ghost attributes work on it unchanged.
|
|
822
|
+
|
|
823
|
+
Most pages should not call it directly — `<om-widget type="ifc-loader">` owns
|
|
824
|
+
the drop zone, the call, the layer elements, the camera and the blob-URL
|
|
825
|
+
lifetime. `examples/bim-workbench.html` is the whole workflow — load, browse, coordinate — in five widget tags.
|
|
826
|
+
|
|
827
|
+
```js
|
|
828
|
+
import { loadIfc, configureIfc } from "@nika-js/onlymap";
|
|
829
|
+
|
|
830
|
+
configureIfc({ wasmPath: "/vendor/web-ifc/" }); // optional: self-host instead of the CDN
|
|
831
|
+
const arch = await loadIfc(archBytes, { onProgress: (m) => console.log(m) });
|
|
832
|
+
// FEDERATION: pass the first model's origin so the two share a frame.
|
|
833
|
+
const mep = await loadIfc(mepBytes, { origin: arch.origin });
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
Returns `tilesetUrl` and `edgesUrl` (blob URLs — assign to a `Tile3DLayer`'s
|
|
837
|
+
`tileset` and a companion `PathLayer`'s `data`), `loadOptions` to pass straight
|
|
838
|
+
through, `features` (the property table, for legends and category lists —
|
|
839
|
+
`ifcClass`, `name`, `material`, `container`, `netVolume`, plus the hierarchy
|
|
840
|
+
columns `spatialPath` / `typePath` / `systemPath` / `classificationPath`, each
|
|
841
|
+
joined by `SPATIAL_SEPARATOR` (U+001F, which cannot occur in an IFC label) and
|
|
842
|
+
emitted only when the file populates it),
|
|
843
|
+
`lonLat`/`georeferenced`/`heading`/`scale`/`originSource`/`headingSource` for
|
|
844
|
+
the `site-*` attributes, `stats`, `timings` (ms per phase), `bounds`, and
|
|
845
|
+
`revoke()`.
|
|
846
|
+
|
|
847
|
+
- **Call `revoke()` when you swap models.** Blob URLs are held by the document
|
|
848
|
+
and are not garbage collected.
|
|
849
|
+
- **`origin` is federation.** Each model is otherwise centred on its own
|
|
850
|
+
bounding box, so two discipline models of one building drift apart by the
|
|
851
|
+
difference between those boxes — and a clash pass then reports nothing, which
|
|
852
|
+
is indistinguishable from a clean model. Pass the first model's `origin` into
|
|
853
|
+
every later `loadIfc` for the same building. `ifc-loader` does this for you
|
|
854
|
+
per map, and shares the PLACEMENT too (`site-origin`/`site-heading`/`site-scale`):
|
|
855
|
+
discipline files routinely declare IfcSite coordinates kilometres apart for the
|
|
856
|
+
same building, so the first model loaded decides where it goes and the rest
|
|
857
|
+
follow. `independent` opts out.
|
|
858
|
+
- Every element also gets a bounding box in the property table
|
|
859
|
+
(`bboxMinE`/`bboxMinN`/`bboxMinU`/`bboxMaxE`/`bboxMaxN`/`bboxMaxU`, tile-local
|
|
860
|
+
metres), which is what `ifc-clash` reads.
|
|
861
|
+
- web-ifc is MPL-2.0 and is NOT a package dependency — it is dynamic-imported
|
|
862
|
+
from unpkg on first use, so it never reaches the bundle and pages that never
|
|
863
|
+
open an IFC pay nothing. Measured: the IFC loader chunk is 23.7 KB raw /
|
|
864
|
+
8.5 KB gzipped and contains only the CDN URL; no web-ifc code and no `.wasm`
|
|
865
|
+
ship in `dist/`.
|
|
866
|
+
- **For OFFLINE, air-gapped or strict-CSP deployments, self-host it.**
|
|
867
|
+
`npm run vendor:web-ifc public/vendor/web-ifc` copies the four files you need (1.37 MB gzipped — the multithreaded pair is included because `IfcAPI.Init()` picks it whenever the page is cross-origin isolated, so vendoring only the single-threaded pair breaks under COOP/COEP),
|
|
868
|
+
then either `wasm-path="/vendor/web-ifc/"` on the `ifc-loader` widget (no
|
|
869
|
+
script needed) or `configureIfc({wasmPath})` once before the first model
|
|
870
|
+
loads. web-ifc is only ONE of the network dependencies though: a georeferenced
|
|
871
|
+
model makes `ifc-loader` switch a basemap on, which reaches a tile server, so
|
|
872
|
+
offline pages also want `basemap="none"` on the loader and `telemetry="off"`
|
|
873
|
+
on the map. They stay STATIC ASSETS fetched by the same lazy import — the JS
|
|
874
|
+
bundle does not grow. Cost: `web-ifc-api.js` 5.31 MB raw / 0.49 MB gzipped
|
|
875
|
+
plus `web-ifc.wasm` 1.20 MB / 0.44 MB, so ~0.93 MB gzipped, served once and
|
|
876
|
+
cached. (`web-ifc-mt.wasm` is a further 1.22 MB and is only needed for the
|
|
877
|
+
multithreaded path.) Note a dynamic import cannot carry subresource
|
|
878
|
+
integrity, so the pinned version in the URL is the only thing fixing what
|
|
879
|
+
runs — another reason to serve it yourself.
|
|
880
|
+
- Position is read from the file, preferring the trustworthy route.
|
|
881
|
+
`IfcMapConversion` — a surveyed placement into a named projected CRS — wins
|
|
882
|
+
over `IfcSite.RefLatitude`/`RefLongitude`, which is very often an authoring
|
|
883
|
+
default. `originSource` says which was used: `"map-conversion"`, `"ifc-site"`
|
|
884
|
+
or `"fallback"`.
|
|
885
|
+
- **Anything other than `"map-conversion"` raises a structured `"warning"`**
|
|
886
|
+
through the same validation channel other `om-layer` errors use (visible in
|
|
887
|
+
the on-page panel with `validate` set, and on `om-validation-error`'s
|
|
888
|
+
`detail.warnings`) — for both `BIMLayer` and the `ifc-loader` widget, once
|
|
889
|
+
per layer. It does not flip `valid` to `false`; it flags that the position
|
|
890
|
+
may be off by tens of metres with no rotation correction applied. Override
|
|
891
|
+
with `site-origin`/`site-heading` once the real location is known, or
|
|
892
|
+
re-export the model with a proper `IfcMapConversion`.
|
|
893
|
+
- Un-projecting a map conversion covers WGS84 UTM zones analytically, plus
|
|
894
|
+
every CRS in the bundled table `src/crs.ts` shares with the CityJSON
|
|
895
|
+
decoder (Dutch RD, Swiss LV95, ETRS89/UTM, Japan's plane systems, …).
|
|
896
|
+
Anything else is declined with a warning naming the bundled codes, rather
|
|
897
|
+
than approximated — a guessed projection lands the model in another
|
|
898
|
+
country while looking entirely plausible.
|
|
899
|
+
- A model aligned to a NATIONAL GRID is not aligned to true north, so the
|
|
900
|
+
grid convergence is measured and folded into the heading. Dutch RD at
|
|
901
|
+
Rotterdam leans -0.735 degrees, and it grows with distance from the
|
|
902
|
+
central meridian.
|
|
903
|
+
- The conversion is APPLIED, not just read. It anchors the model's ORIGIN,
|
|
904
|
+
while the tileset is recentred on its geometry, so the full affine
|
|
905
|
+
(offset, grid axis, scale) is applied to the anchor point. Skipping that
|
|
906
|
+
put one Revit export 32.7 m out, its survey point being that far from the
|
|
907
|
+
building.
|
|
908
|
+
- `IfcMapConversion` OUTRANKS `IfcSite` for position. Files carrying both
|
|
909
|
+
routinely disagree: one Dutch model's two statements are 108 m apart, and
|
|
910
|
+
the projected pair is the surveyed one. Note the eastings/northings are in the target CRS's own
|
|
911
|
+
unit, which is frequently MILLIMETRES.
|
|
912
|
+
- `headingSource` distinguishes `"map-conversion"` / `"true-north"` (read from
|
|
913
|
+
the file) from `"assumed"` (the file was silent and project north was taken as
|
|
914
|
+
true north). Report the assumption; do not let it read as a measurement.
|
|
915
|
+
- A file can be perfectly georeferenced and still land somewhere useless: all
|
|
916
|
+
three prepared samples in this repo declare placeholder positions (the clinic
|
|
917
|
+
on Revit's Boston default, which reverse-geocodes to a 1630 graveyard; the
|
|
918
|
+
duplex on a Chicago city-centre point; the bridge, which DOES carry a real
|
|
919
|
+
`IfcMapConversion`, into the mid-Pacific at 179.08E 8.46S). Reading the file
|
|
920
|
+
correctly and the model being in a sensible place are separate problems, and
|
|
921
|
+
`site-origin` is the fix for the second.
|
|
922
|
+
- `<om-map>` reads its camera attributes ONCE at init, so `setAttribute("center", …)`
|
|
923
|
+
after mount moves nothing and leaves the model outside the frustum. Use
|
|
924
|
+
`map.flyTo(lonLat, zoom)`.
|