@nika-js/onlymap 0.6.0 → 0.6.2
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 +63 -1
- package/README.md +39 -14
- package/THIRD-PARTY-LICENSES.md +27 -0
- package/dist/{LercDecode.es-fbo6h5rt.js → LercDecode.es-BOJJr6Gx.js} +1 -1
- package/dist/{basemap-CprSan9q.js → basemap-BofXgXxy.js} +13 -3
- package/dist/basemap.d.ts +10 -1
- package/dist/cityjson-D_V5GY8b.js +332 -0
- package/dist/clip-box-controller.d.ts +94 -0
- package/dist/clip-box.d.ts +109 -0
- package/dist/crs-Ciu7Xs7a.js +108 -0
- package/dist/crs.d.ts +50 -0
- package/dist/ctx.d.ts +8 -1
- package/dist/data-layer.d.ts +11 -3
- package/dist/draw-controller.d.ts +75 -1
- package/dist/draw.d.ts +22 -4
- package/dist/elements/om-map.d.ts +45 -2
- package/dist/elements/om-overlay.d.ts +13 -1
- package/dist/elements/om-widget.d.ts +4 -0
- package/dist/feature-colors.d.ts +41 -0
- package/dist/geodesy.d.ts +29 -3
- package/dist/{geoparquet-Bm8pfxyo.js → geoparquet-DapHATA0.js} +1 -1
- package/dist/html-data.d.ts +2 -2
- package/dist/ifc-Dc-TdSJr.js +877 -0
- package/dist/ifc.d.ts +406 -0
- package/dist/{index-Bz-rbmhj.js → index-7B-6Cbzu.js} +31619 -26565
- package/dist/{index-DSRIZR81.js → index-BF9iO8Tq.js} +1 -1
- package/dist/{index-GOwMjqm6.js → index-BmX6IId3.js} +1 -1
- package/dist/{index-UzmNhuS0.js → index-SPJVn_n_.js} +1 -1
- package/dist/{index-CCQttJzO.js → index-xmJjZxQJ.js} +2 -2
- package/dist/index.d.ts +13 -2
- package/dist/ir-snapshot.d.ts +3 -1
- package/dist/layer-registry.d.ts +14 -0
- package/dist/layers/bim-layer.d.ts +127 -0
- package/dist/layers/feature-mesh-layer.d.ts +288 -0
- package/dist/layers/gltf-scene-walk.d.ts +55 -0
- package/dist/layers/popup-layer.d.ts +13 -0
- package/dist/legend-spec.d.ts +1 -1
- package/dist/{lerc-CuifOkoA.js → lerc-C6k7EzSN.js} +2 -2
- package/dist/license.d.ts +12 -5
- package/dist/measure-controller.d.ts +458 -4
- package/dist/meshopt_simplifier-Co6uRDDA.js +370 -0
- package/dist/onlymap.standalone.js +57077 -50729
- package/dist/onlymapjs.js +72 -69
- package/dist/parse-manifest.d.ts +3 -0
- package/dist/programmatic.d.ts +39 -5
- package/dist/{raster-dysYIQ_k.js → raster-Cuzhibe2.js} +2 -2
- package/dist/{raster-pipeline-ZdCOGfqV.js → raster-pipeline-DLrnJK8y.js} +1 -1
- package/dist/react/om-layer.d.ts +6 -1
- package/dist/react/om-overlay.d.ts +9 -0
- package/dist/react.js +176 -169
- package/dist/region-export-controller.d.ts +37 -0
- package/dist/region-export.d.ts +56 -0
- package/dist/runtime-core.d.ts +260 -3
- package/dist/selection.d.ts +44 -1
- package/dist/site-placement.d.ts +107 -0
- package/dist/snapping.d.ts +88 -0
- package/dist/terrain-heightfield.d.ts +53 -0
- package/dist/terrain-sample.d.ts +30 -0
- package/dist/terrain.d.ts +6 -1
- package/dist/testing.d.ts +6 -2
- package/dist/tile3d-metadata.d.ts +185 -0
- package/dist/units.d.ts +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/volumetrics-run.d.ts +13 -0
- package/dist/volumetrics-worker.d.ts +1 -0
- package/dist/volumetrics.d.ts +201 -0
- 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-CUzEX8fB.js} +79 -66
- package/docs/3d-assets.md +48 -0
- package/docs/live-data.md +9 -1
- package/docs/testing.md +4 -2
- package/llms.txt +10 -5
- package/onlymapjs.html-data.json +313 -2
- package/package.json +6 -2
- package/skills/onlymapjs/SKILL.md +14 -2
- package/skills/onlymapjs/references/react.md +2 -2
- package/skills/onlymapjs/references/syntax.md +314 -15
- package/dist/cityjson-urQeujQv.js +0 -407
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nika-js/onlymap",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Declarative deck.gl maps for HTML and React — interactive WebGL mapping with GeoJSON/CSV/Arrow data, MapLibre basemaps, widgets, popups, and live streams from a custom-element manifest or typed React components. TypeScript, no build step.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"author": "NikaGeospatial",
|
|
10
|
-
"homepage": "https://
|
|
10
|
+
"homepage": "https://onlymap.nikaplanet.com",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "git+https://github.com/NikaGeospatial/onlymapjs.git"
|
|
@@ -116,9 +116,11 @@
|
|
|
116
116
|
"test:e2e": "playwright test",
|
|
117
117
|
"check-layout": "playwright test e2e/layout-audit.spec.ts",
|
|
118
118
|
"gen:html-data": "vite-node dev/generate-html-data.ts",
|
|
119
|
+
"vendor:web-ifc": "node dev/tools/vendor-web-ifc.mjs",
|
|
119
120
|
"gen:public": "npm run gen:html-data && vite-node dev/build-public.ts",
|
|
120
121
|
"gen:site": "vite-node dev/build-site.ts",
|
|
121
122
|
"test:public": "npm run gen:public -- --dry-run --strict",
|
|
123
|
+
"test:site": "npm run gen:public -- --out dist-public --strict && npm run gen:site -- --in dist-public --dry-run --strict --site-url https://onlymap.nikaplanet.com --cname onlymap.nikaplanet.com",
|
|
122
124
|
"deploy:telemetry": "wrangler deploy --config cloud/workers/telemetry/wrangler.toml",
|
|
123
125
|
"dev:telemetry": "wrangler dev --config cloud/workers/telemetry/wrangler.toml",
|
|
124
126
|
"deploy:examples": "wrangler deploy --config cloud/workers/examples/wrangler.toml",
|
|
@@ -139,6 +141,7 @@
|
|
|
139
141
|
"@loaders.gl/csv": "^4.4.3",
|
|
140
142
|
"@loaders.gl/kml": "^4.4.3",
|
|
141
143
|
"@loaders.gl/shapefile": "^4.4.3",
|
|
144
|
+
"@luma.gl/engine": "9.3.5",
|
|
142
145
|
"@playwright/test": "^1.61.1",
|
|
143
146
|
"@reduxjs/toolkit": "^2.12.0",
|
|
144
147
|
"@types/d3-array": "^3.2.2",
|
|
@@ -163,6 +166,7 @@
|
|
|
163
166
|
"hyparquet-compressors": "^1.1.1",
|
|
164
167
|
"jotai": "^2.20.2",
|
|
165
168
|
"maplibre-gl": "^5.24.0",
|
|
169
|
+
"meshoptimizer": "^1.2.0",
|
|
166
170
|
"mobx": "^6.16.1",
|
|
167
171
|
"playwright": "^1.61.1",
|
|
168
172
|
"proj4": "^2.20.8",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onlymapjs
|
|
3
|
-
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
3
|
+
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OnlyMapJS
|
|
@@ -23,7 +23,7 @@ Use OnlyMapJS as a declarative HTML map library. Write custom elements such as `
|
|
|
23
23
|
</script>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.
|
|
26
|
+
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.2` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.2/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
|
|
27
27
|
|
|
28
28
|
## React Projects
|
|
29
29
|
|
|
@@ -35,6 +35,10 @@ import { OmMap, OmLayer, OmWidget, OmOverlay, useOmMap } from "@nika-js/onlymap/
|
|
|
35
35
|
|
|
36
36
|
The adapter inverts several HTML-manifest rules: props are camelCase deck.gl props, accessors are plain JS functions (`getFillColor={d => ...}` — no `$field` expression language, no `js` opt-in), and interactions are `onClick`/`onHover` handlers plus React state, not `<om-behavior>` or state-mutating actions. Load `references/react.md` before writing React map code.
|
|
37
37
|
|
|
38
|
+
## Native and JSON Bridges
|
|
39
|
+
|
|
40
|
+
When a trusted native/cross-process host drives `MapController`, keep descriptors JSON-safe. A schema-declared accessor prop may use the same restricted expression grammar as HTML, for example `props: { getPosition: "[$lon, $lat]" }`; ordinary scalar string props remain strings. Use `snapshotDescriptorIR(descriptors)` for deterministic parity checks without fetching URL data. Call `controller.suspend()` on background and `resume()` on foreground; removing/changing a live descriptor or destroying the controller releases its reference-counted fetch/poll/socket handle. React components should continue using function accessors. For app-scoped licenses, pass `configureLicense(key, { appId })` only with an identifier obtained from trusted platform build metadata, never from bridge/page input.
|
|
41
|
+
|
|
38
42
|
## Required References
|
|
39
43
|
|
|
40
44
|
Load the smallest reference needed for the task:
|
|
@@ -73,13 +77,21 @@ Load the smallest reference needed for the task:
|
|
|
73
77
|
- Group adjacent map buttons (zoom + undo + toggle into one control group) -> just place compact button widgets in the same `position` slot; they auto-cluster. `cluster="false"` opts one out. Do NOT build a wrapper widget.
|
|
74
78
|
- GeoTIFF/COG raster (DEM, satellite imagery, NDVI) -> `<om-layer type="COGLayer" src="…tif">` with `min`/`max`/`colormap` for single-band data (see syntax.md — `src`, not `data`).
|
|
75
79
|
- Zarr / GeoZarr raster (chunked N-D arrays — climate/weather grids, datacubes) -> `<om-layer type="ZarrLayer" src="….zarr" variable="…" select="time=0">` with `min`/`max`/`colormap` (same raster styling as COGLayer). Pick the `variable` and pin every non-spatial dim in `select`. A GeoZarr store georeferences itself; a plain Zarr needs manual `bounds` + `crs` + `spatial-dims`. `src`, not `data` — and it can be any absolute URL to a remote store, which needs no server setup but must be CORS-enabled (fetched in the browser) and public (authenticated stores are a follow-up). See syntax.md.
|
|
80
|
+
- BIM model / .ifc file / 3D Tiles with per-element picking -> `<om-layer type="BIMLayer" src="….ifc" pickable>` (parses the IFC in-browser; web-ifc WASM is CDN-fetched on first use) or `type="Tile3DLayer" tileset="…" pick-features` for a pre-built tileset. Author `terrain` explicitly (`terrain="mapterhorn"`, or `terrain="off"` for flat ground) — a model that resolves real elevation (IfcMapConversion + OrthogonalHeight) on a terrain-less map raises an error through the validation channel at load time, and the library never writes attributes for you. Style by property with `feature-color-by`/`feature-color-scale` (+ `feature-palette`); isolate/hide/ghost elements declaratively with `feature-filter-field` + `isolate-features`/`hide-features`/`ghost-features` (undoable, story-steppable). See syntax.md.
|
|
81
|
+
- Browse/inspect/clash-check BIM models -> widgets: `ifc-loader` (drop zone, `federate` for multi-model coordination), `ifc-browser` (group/count/colour by any property field), `feature-inspector` (per-element properties on pick; `ifc-inspector` is an alias), `ifc-clash` (AABB clash overlay between two co-registered models). See syntax.md.
|
|
76
82
|
- Geotagged drone JPEG -> `<om-layer type="ImageOverlay" src="…jpg" georeference="exif">`; for saved/collaborative maps persist the processed image and reconstruct with explicit `bounds` (see syntax.md; this is visualization-grade, not orthorectification).
|
|
77
83
|
- Dashed line/route/boundary (or any dashed stroke) -> the `dash` attribute on a path-stroking layer: `dash="[6, 3]"` (or SVG-style `dash="6 3"`, + optional `dash-justified`) on `PathLayer`/`GeoJsonLayer`/`PolygonLayer`/`TripsLayer`. `[dashLength, gapLength]` in line-width units. Do NOT hand-wire deck's `PathStyleExtension`/`getDashArray` — the attribute mounts it; `dash` on a non-path layer is ignored with a warning.
|
|
78
84
|
- CityJSON/CityJSONSeq per-face surfaces mode (`?om-surfaces=1`) -> always pair the `SolidPolygonLayer` with a companion `<om-layer type="PathLayer">` using `get-path="$outline"` to make roof/wall edges visible. This mode is unlit and `SolidPolygonLayer`'s own `wireframe` prop does nothing here (deck only builds wireframe geometry when `extruded: true`); the `PathLayer` is the only way to see face edges. Give it the same `filter-field`/`filter-range` as the fill layer so filtered-out buildings' outlines disappear too. See syntax.md.
|
|
79
85
|
- Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
|
|
80
86
|
- REST snapshot that changes over time -> `refresh="5s"`.
|
|
81
87
|
- User sketching -> `data="draw:sketch"` layer plus `<om-widget type="draw" target="sketch">`.
|
|
88
|
+
- Export part of a loaded 3D model (BIM/`Tile3DLayer`) as a portable file (issue #34) -> `<om-widget type="draw" modes="polygon" target="<name>" export-3d>` (or `export-3d="b3dm"` for Cesium/3D-Tiles pipelines). Outline a footprint over the loaded content, close it, "Export 3D" clips every loaded tile's triangles to that footprint (plain 2D clip — no elevation-picking needed) and downloads a GLB re-framed to a local coordinate frame at the footprint's own centroid, each triangle carrying its own source color (vertex colors). Separate capability from `save` (that's the drawn shape's GeoJSON, not the 3D content inside it). No textures — BIM/IFC materials are flat colors, not textured meshes. Only currently-visible 3D Tiles/BIM layers are included in the export — hiding a layer (`visible="false"`, or the `toggle-layer` action) excludes it, with distinct console warnings for "nothing loaded yet" vs. "everything hidden."
|
|
89
|
+
- Cut into / reveal the inside of a loaded 3D scene (BIM model, 3D Tiles, or any layer) with a box (issue #34) -> `<om-map clip-box-min="[lng,lat,elev]" clip-box-max="[lng,lat,elev]">` clips every layer to that axis-aligned box by default (`clip="off"` on an `<om-layer>` opts it out); `clip-box-invert` shows the outside instead, `clip-box-highlight` dims clipped-out geometry rather than discarding it (non-destructive preview). Works on any layer type including georeferenced `Tile3DLayer`/`BIMLayer` content — the point is cutting into a dense BIM scene, not just flat GeoJSON extrusions. Attribute-backed (undoable, story-steppable) via the `set-clip-box {min, max, invert?, highlight?}` action (`{clear: true}` removes it); `<om-widget type="clip-box">` is the native UI — six number inputs (min/max × lng/lat/elevation) + invert/highlight checkboxes + a clear button. v1 is axis-aligned only; rotated boxes are a documented follow-up, not this release.
|
|
90
|
+
- Snap a drawn/measured vertex to a nearby feature's own vertex/edge/midpoint (issue #34 Part A) -> `<om-map snap="vertex edge midpoint" snap-tolerance="12">` (px, default 12). NOT a spatial index — it refines whatever feature deck's own hover/click pick already found under the cursor (free, every frame) to that ONE feature's nearest vertex/edge/edge-midpoint, on the CPU, only while snapping is on. Applies to every layer by default (`snap="off"` on any `<om-layer>` opts it out, same shape as `clip="off"`) AND to a `BIMLayer`'s own edge/crease overlay (its real wall corners/edges, converted from the model's local mesh coordinates to real `[lng,lat]` automatically) — the raw triangle MESH itself is not a snap target (no comparable "nearest vertex" concept for an arbitrarily-picked point on a dense surface). Vertex beats midpoint beats edge on range conflicts; Space suppresses snapping momentarily (standard CAD/GIS convention). No action to wire — it's live-reactive like `terrain`/`clip-box-*`, and works automatically with any drawing/measuring tool that already routes through `om-map-point`.
|
|
82
91
|
- Measure geodesic distance or area (a ruler / area tool) -> `<om-widget type="measure" modes="distance area" units="metric|imperial|nautical">`. Click the map to place points; it shows live per-segment + total labels and dispatches an `om-measure` event (`detail` = the readout). Reuses the draw capture stack, so measure and draw are mutually exclusive. Do NOT hand-roll distance math off canvas pixels; the `scale-bar` widget also takes `units` now.
|
|
92
|
+
- Measure cut/fill volume (earthworks/stockpiles — how much material a shape holds, or how much to add/remove to reach a target elevation) -> `modes="distance area volume"` on the same `measure` widget. Outline a footprint (closes like `area`); the math is a REAL per-cell grid integration against the map's terrain DEM (metric tangent-plane grid at the DEM's own GSD, scanline point-in-polygon, bilinear seam-correct sampling, worker-offloaded) — mixed cut AND fill within one footprint on undulating ground, with `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD) and `nodataFraction` published on the readout. `base-surface` picks the reference: `custom` (default — a draggable gizmo target plane, re-summed live per drag frame) or boundary-derived stockpile strategies with NO gizmo (`triangulated` boundary TIN — recommend this for "measure this pile/mound", `plane`, `lowest`, `highest`, `average`). Reads out `cutMeters3`/`fillMeters3`/`netMeters3` (signed, fill−cut)/`totalMeters3` (unsigned, cut+fill) — always RAW geometric volumes, never altered by `swell`/`shrink`. REQUIRES `terrain` on `<om-map>` — a `volume` mode with none is a validation warning (falls back to a flat-plane approximation with no error figures). Volume-only attributes, all no-ops (validation warns) without `volume` in `modes`: `base-surface`; `profile` (elevation samples around the footprint's own perimeter, live while sketching — dispatched on `profileSeries` for a `dynamic-chart` widget to plot — that widget is generic: `<om-widget type="dynamic-chart" on="om-measure" series-field="profileSeries" width="320">` reads `event.detail[seriesField]` on every matching event and redraws, and "freezes" for free when a later event simply omits that field); `deadband` (zeroes a Cut/Fill figure below the threshold); `density`/`swell`/`shrink` populate a SEPARATE Material section (Bank/Loose/Compacted convention — `cutAdjustedMeters3` = raw × swell, `fillAdjustedMeters3` = raw ÷ shrink, `cutMassKg`/`fillMassKg` from the raw, mass-conserving volume), shown only once one of the three is configured — never baked into the primary Cut/Fill/Net/Total numbers. A `stale` readout field flags the window between a footprint committing and its integration resolving.
|
|
93
|
+
- Need a real ELEVATION on a click/hover (a z readout, a tooltip showing the height of the building face under the cursor, a coordinate that lands on 3D content rather than the ground behind it) -> `pickable="3d"` on the layer instead of a bare `pickable` (issue #34): it opts the layer into deck's depth-pick pass, and the resolved coordinate carries a third component. Read it as `{{z}}` in an `<om-overlay>` / `show-tooltip` template or `ctx.selection.coordinate` in a widget script. `terrain` sets this on itself. `{{z}}` is ABSENT (not `0`) when no layer in the scene ran the depth pass for that pick — do not treat a missing elevation as sea level.
|
|
94
|
+
- Small transient overlay that tracks the cursor near a map edge (a snap tip, a live readout badge) -> add `clip-to-map` to the `<om-overlay>`: it hides when the overlay's own BOX would spill past the map viewport, not just when its anchor leaves. Without it an overhanging absolutely-positioned box inflates the page's scrollable overflow and the scrollbar -> map resize -> reprojection loop shows as view jitter. Opt-in on purpose — an authored popup near an edge normally wants to keep showing its visible half.
|
|
83
95
|
- Capture raw map clicks/hovers yourself (measure distance, drop a pin where the user clicks, a custom rectangle/circle AOI, snap-to-feature) -> listen for the **`om-map-point`** DOM event on `<om-map>`: `mapEl.addEventListener('om-map-point', e => { const { coordinate, kind } = e.detail; /* [lng,lat] or null; kind is "click"|"hover" */ })`. It fires on EVERY click/hover including empty-map clicks. NEVER read deck.gl internals (`getMap()`, `deckInstance`, `deck.viewManager`) or unproject canvas pixels by hand — those are not on `<om-map>` and will silently return nothing. The built-in `draw` widget covers polygon/line/point sketching; `om-map-point` is for tools it doesn't. (`MapController` twin: the `onMapPoint` option. See patterns.md.)
|
|
84
96
|
- Page may travel as a file (shared, emailed, downloaded) or be embedded -> add an `<om-fallback>` child to `<om-map>`. Chat-app and email previews render HTML with JavaScript disabled (iOS QuickLook), so the map cannot boot there; the fallback is what recipients see instead. It is hidden automatically once the map boots. Good practice on every complete page — without one, the stylesheet shows a generic text-only banner.
|
|
85
97
|
|
|
@@ -49,9 +49,9 @@ function StatsPanel({ onToggle }) {
|
|
|
49
49
|
## Component surface
|
|
50
50
|
|
|
51
51
|
- **`<OmMap>`** — `center`/`zoom`/`pitch`/`bearing` (initial; later changes move the camera, unchanged props never fight user panning), `basemap`, `headless`, `widgetStyle` (layout-token sugar, the `widget-style` attribute's twin: `"gap:10 opacity:0.9"` → `--om-widget-*` custom properties), `widgetsHidden` (hide-all: hides authored managed widget wrappers without removal, so widget state survives; provider attribution and the license badge stay visible in their slots), `widgetsFold` (default true; map-width responsive side drawers, set false to opt out), `onReady`, `onViewStateChange`, `onRuntimeError`. Give it a size via `style`/`className`. `ref` exposes the imperative `MapController` handle: `flyTo`, `setView`, `emit`, `getLayers`, `getSelection`, `injectPick`, `ready` (promise), `project`.
|
|
52
|
-
- **`<OmLayer>`** — `id` + `type` (any registered deck.gl layer type) + deck props. `data`: stable inline reference or URL string (full Data Layer: CSV/Arrow/Shapefile/KML formats, `ws(s)://` streams via `source`/`streamKey`/`flush`, `refresh` polling). `label`/`color` feed `ctx.layers`; `filterField`/`filterRange` = GPU filter; `onClick`/`onHover` receive the flattened picked object (`onHover(null)` = pointer left).
|
|
52
|
+
- **`<OmLayer>`** — `id` + `type` (any registered deck.gl layer type) + deck props. `data`: stable inline reference or URL string (full Data Layer: CSV/Arrow/Shapefile/KML formats, `ws(s)://` streams via `source`/`streamKey`/`flush`, `refresh` polling). `dash`/`dashJustified` mirror the HTML path-style attributes. `label`/`color` feed `ctx.layers`; `filterField`/`filterRange` = GPU filter; `onClick`/`onHover` receive the flattened picked object (`onHover(null)` = pointer left). Conditional unmount/removal releases that layer's live-transport ownership; the final owner closes it.
|
|
53
53
|
- **`<OmWidget>`** — positioning shell: `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) + arbitrary JSX. Same-slot widgets stack with flush edges and a shared gap; at map widths ≤640px they auto-fold into per-side drawers (`fold="never"` exempts an essential control); provider attribution joins `bottom-end` and the license badge joins `bottom-start` as in-flow members; `order={1}` sets deterministic in-slot ordering. `position="manual"` renders a plain block at the JSX site — note it sits inside OmMap's overflow-hidden box, so for UI OUTSIDE the map render your own element next to `<OmMap>` and drive the map via `useOmMap()`/the ref instead. (Automatic button-clustering of adjacent compact widgets, and collision-dim of slots under an open overlay, are HTML-lane only — in React, compose your own control group in JSX and dim via your own state.)
|
|
54
|
-
- **`<OmOverlay>`** — geo-anchored HTML with managed projection/tracking/culling. `anchor={[lng, lat]}` or `anchorFrom="selection"` (+ `layer` to scope which picks move it); children may be `(selection) => JSX`; `anchorOffset` (default `bottom-center`); `interactive={false}` for hover-following tooltips.
|
|
54
|
+
- **`<OmOverlay>`** — geo-anchored HTML with managed projection/tracking/culling. `anchor={[lng, lat]}` or `anchorFrom="selection"` (+ `layer` to scope which picks move it, + `selectionType="click"|"hover"` to scope which pick TYPE — a click-opened popup wants `selectionType="click"`, else hovering any pickable feature drags and re-renders it; a click on empty space still dismisses); children may be `(selection) => JSX`; `anchorOffset` (default `bottom-center`); `interactive={false}` for hover-following tooltips.
|
|
55
55
|
- **`useOmMap(watch?)`** — the same `ctx` contract HTML widget scripts get, typed: `layers`, `viewport`, `selection`, `emit`, `data()`, `dataInViewport()`, `stats()`. Watch tokens: `"viewport"`, `"selection"`, `"layers"`, `"data:<layerId>"`. Rides `useSyncExternalStore` (tearing-safe; `ctx` identity stable until a watched token fires).
|
|
56
56
|
- **`controller.getStore(token)`** — framework-free `{subscribe, getSnapshot}` per watch token (cached plain-data snapshots; viewport snapshots carry `origin: "user"|"programmatic"` for echo-free two-way binding). Use it to sync map state into Redux/MobX/Zustand/Jotai — recipes in docs/external-stores.md; never mirror row arrays into a store (use the `data:<id>` version stamp + `ctx.data()`).
|
|
57
57
|
|
|
@@ -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.2/dist/onlymapjs.css">
|
|
20
|
+
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.6.2"></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,8 +42,10 @@ 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
|
+
- `clip-box-min="[lng, lat, elevation]"` + `clip-box-max="[lng, lat, elevation]"` (issue #34) — a real axis-aligned 3D box clipping the whole scene: geometry outside it is discarded (order of the two corners doesn't matter). Every layer is clipped once a box is configured; opt a specific layer out with `clip="off"` on its `<om-layer>`. `clip-box-invert` shows what's OUTSIDE the box instead; `clip-box-highlight` dims clipped-out geometry instead of discarding it (non-destructive preview — nothing disappears). Works on ANY layer type, including georeferenced `Tile3DLayer`/`BIMLayer` content, so a dense BIM scene can be cut open to see what's inside — not just flat `GeoJsonLayer` extrusions. Attribute-backed (undoable, story-steppable) via `set-clip-box {min, max, invert?, highlight?}` (`{clear: true}` removes the box) and `<om-widget type="clip-box">` (six number inputs + invert/highlight checkboxes + clear button). v1 is axis-aligned only — rotated boxes are a documented follow-up.
|
|
48
|
+
- `snap="vertex edge midpoint"` + `snap-tolerance="12"` (issue #34 Part A) — XY snapping for draw/measure vertex capture: refines whatever deck already picked under the cursor to the nearest vertex, edge, or edge midpoint of THAT feature's own geometry, within `snap-tolerance` pixels (default 12) — not a spatial index, only the already-picked feature is searched. Applies to every layer by default; `snap="off"` on any `<om-layer>` opts it out, mirroring `clip="off"`. Also snaps to a `BIMLayer`'s own edge/crease overlay (its real wall corners/edges, converted from local mesh coordinates automatically) — the raw triangle mesh itself is not a snap target. Vertex beats midpoint beats edge on range conflicts. Hold Space to place a point nearby without snapping. No action/attribute-backed toggle beyond the attribute itself; live-reactive like `terrain`/`clip-box-*`.
|
|
47
49
|
- `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
50
|
- `headless width="800" height="600"` for test harness use
|
|
49
51
|
|
|
@@ -71,7 +73,7 @@ Core attributes:
|
|
|
71
73
|
- `type="ScatterplotLayer"` — any bundled layer type.
|
|
72
74
|
- `data="./points.json"` — URL, stream, draw store, or omit for inline JSON.
|
|
73
75
|
- `label="Earthquakes"` and `color="#b30000"` — legend metadata.
|
|
74
|
-
- `pickable` — enable click/hover behaviors.
|
|
76
|
+
- `pickable` — enable click/hover behaviors. `pickable="3d"` (issue #34) additionally opts the layer into deck's DEPTH-pick pass, so a click/hover's resolved coordinate carries a real third (elevation) component instead of the ray∩z=0-plane guess a flat pick gives you — a click on a building face resolves ON the face, not on the ground behind it. `terrain` sets this on itself. The elevation flows through `ctx.selection.coordinate` and `{{z}}` in `<om-overlay>`/`show-tooltip` templates; `{{z}}` is ABSENT (not `0`) when no layer in the scene ran the depth pass for that pick, so "no elevation" is distinguishable from sea level.
|
|
75
77
|
- `visible="false"` or `opacity="0"` — initial visibility/opacity.w
|
|
76
78
|
|
|
77
79
|
Accessors:
|
|
@@ -137,7 +139,7 @@ For an epoch-millisecond filter, format the built-in widget's numeric labels dec
|
|
|
137
139
|
|
|
138
140
|
Use the `type` value exactly:
|
|
139
141
|
|
|
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`.
|
|
142
|
+
`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
143
|
|
|
142
144
|
Common choices:
|
|
143
145
|
|
|
@@ -147,6 +149,7 @@ Common choices:
|
|
|
147
149
|
- Aggregation: `HeatmapLayer`, `HexagonLayer`, `GridLayer`, `ScreenGridLayer`.
|
|
148
150
|
- Tiles: `TileLayer`, `MVTLayer`, `Tile3DLayer`.
|
|
149
151
|
- 3D models: `ScenegraphLayer`, `SimpleMeshLayer`, `PointCloudLayer`, `Tile3DLayer`.
|
|
152
|
+
- BIM source files (`.ifc`, loaded in-browser, no pre-conversion step): `BIMLayer`.
|
|
150
153
|
- GeoTIFF/COG rasters: `COGLayer`.
|
|
151
154
|
- Zarr / GeoZarr rasters (chunked N-D arrays): `ZarrLayer`.
|
|
152
155
|
- Geotagged drone JPEGs: `ImageOverlay`.
|
|
@@ -163,7 +166,7 @@ Common choices:
|
|
|
163
166
|
- `src` (required) — the GeoTIFF URL. NOT `data`: rasters stream tiles by HTTP Range request through the layer's own reader; they are never parsed rows (`$field`, `ctx.data()`, `ctx.stats()`, filters do not apply).
|
|
164
167
|
- Sources must be Cloud-Optimized GeoTIFFs (`gdal_translate -of COG` otherwise).
|
|
165
168
|
- `min`/`max` — the rescale window mapped onto the colormap. Defaults to 0–255, so ALWAYS set them for float or 16-bit data (DEMs, NDVI, temperature).
|
|
166
|
-
- `colormap` — single-band ramps from the bundled sprite: `gray` (default), `viridis`, `plasma`, `inferno`, `magma`, `cividis`, `rdylgn`, `rdbu`, `spectral`, `terrain`, `jet`, `turbo
|
|
169
|
+
- `colormap` — single-band ramps from the bundled sprite: `gray` (default), `viridis`, `plasma`, `inferno`, `magma`, `cividis`, `rdylgn`, `rdbu`, `spectral`, `terrain`, `jet`, `turbo`, `ylorrd` (ColorBrewer yellow→orange→red — a gentler sequential ramp than turbo/jet). Sources with 3+ bands composite as RGB and ignore it.
|
|
167
170
|
- `nodata` — overrides the source's nodata sentinel; nodata pixels render transparent.
|
|
168
171
|
- Plain 8-bit RGB COGs (satellite truecolor) need no styling attributes at all.
|
|
169
172
|
- Restretch/recolor (min/max/colormap edits) are GPU uniform updates — tiles are not refetched. The legend widget renders the colormap ramp automatically when `colormap` + `min`/`max` are authored.
|
|
@@ -351,14 +354,24 @@ Built-ins:
|
|
|
351
354
|
- `scale-bar` — `units="metric|imperial|nautical"` (default metric) picks the length system; snaps to a nice round distance.
|
|
352
355
|
- `attribution`
|
|
353
356
|
- `filter`
|
|
354
|
-
- `draw`
|
|
355
|
-
- `
|
|
357
|
+
- `draw` — sketch-capture toolbar: `modes="point line polygon"` (default all three), `target="<name>"` (default `sketch`, bound via `data="draw:<target>"`), `save="both|download|file-system"`, `autosave="<localStorage key>"`. `export-3d` (bare = GLB default, `="b3dm"` wraps it for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (spec: issue #34 — region export) — deliberately separate from `save` (that's the drawn shape's own GeoJSON; `export-3d` exports the 3D `Tile3DLayer`/`BIMLayer` content found INSIDE the drawn footprint). Outline a polygon over loaded 3D content, close it, click "Export 3D": clips every loaded tile's triangles to the footprint (a plain 2D clip — no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's own centroid (portable — opens correctly in Blender/three.js/etc. without ECEF-scale support), and downloads it, each triangle carrying its own source color (baked as vertex colors). No textures — BIM/IFC materials are flat colors, not textured meshes. The export only pulls in currently-VISIBLE 3D Tiles/BIM layers — one hidden via `visible="false"` (or the `toggle-layer` action) is excluded, with a distinct console warning distinguishing "nothing has loaded yet" from "everything loaded is hidden." Validation warns on an unrecognized `export-3d` value.
|
|
358
|
+
- `clip-box` — native UI over the map's `clip-box-*` scene-state attributes (see the `<om-map>` section above): six number inputs (min/max × lng/lat/elevation), invert/highlight checkboxes, and a clear button, all wired through `set-clip-box`. Manifest is the source of truth — the panel re-syncs from the attributes on every render, so undo/redo and story-scrub move the inputs too.
|
|
359
|
+
- `measure` — geodesic ruler: `modes="distance area volume"` (space-separated; default `distance area`), `units="metric|imperial|nautical"`. Click the map to place points; live per-segment + total labels render on the map, and a totals panel + a `units` toggle sit in the widget. Distance is haversine on the WGS84 mean sphere (≤0.56% vs. the true geodesic); area is the spherical-excess integral. Nautical shows nmi for length and falls back to metric for area. Reuses the draw capture stack (measure and draw are mutually exclusive); the geometry is ephemeral (never saved, never an undo step). Consume the reading programmatically via the `om-measure` event on `<om-map>` (`detail = {mode, units, totalMeters, segments, areaMeters2, perimeterMeters, poleWarning, cutMeters3, fillMeters3, netMeters3, totalMeters3, cutAdjustedMeters3, fillAdjustedMeters3, swell, shrink, cutMassKg, fillMassKg, cellSizeM, gsdM, cutErrorM3, fillErrorM3, nodataFraction, baseSurface, stale, profileSeries}` — everything from `cutMeters3` on is volume-mode-only, populating once a footprint closes). `volume` mode outlines a polygon footprint the same way `area` does — double-click (or Enter) closes it, and it turns solid teal to signal it's ready — then a double-headed arrow gizmo (fixed screen-pixel size, unbounded drag distance) appears at the centroid: drag it up to fill, down to cut, panel reads Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) volume + Area/Perimeter live — always RAW geometric figures, never altered by `swell`/`shrink`. REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none): the math is a REAL per-cell grid integration (issue #35) — closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD at the ring's latitude, scanline point-in-polygon, bilinear tile-seam-correct sampling, worker-offloaded with a synchronous fallback), reporting mixed cut AND fill within one footprint on undulating ground plus `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, summed per side) and `nodataFraction` on the readout. `base-surface` picks the reference surface: `custom` (default — the gizmo's draggable target plane, re-summed live from the cached grid during a drag) or boundary-derived stockpile strategies with NO gizmo (`triangulated` boundary TIN — the drone-survey default, `plane` least-squares, `lowest`/`highest`/`average`). No terrain (or a failed tile fetch) falls back to the flat single-elevation approximation with no error figures rather than erroring. Five more volume-only attributes, all no-ops without `volume` in `modes` (validation warns): `base-surface` (above); `profile` — closing a footprint also samples elevation around its own perimeter, dispatched on `profileSeries` for a paired `dynamic-chart` widget to plot, updating live from the first vertex (debounced on hover, immediate on each new vertex) while sketching, not just on close; `deadband` (m³, default 0) — zeroes a Cut/Fill figure below the threshold; `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a SEPARATE Material section instead of touching Cut/Fill/Net/Total — standard Bank/Loose/Compacted convention: `cutAdjustedMeters3` = raw cut × swell (loose/haul volume, bigger — excavating adds air voids), `fillAdjustedMeters3` = raw fill ÷ shrink (loose/borrow volume needed, also bigger — raw fill is already a compacted target void), `cutMassKg`/`fillMassKg` from the RAW volume (mass-conserving — swell/shrink change volume via air voids, not the mass of material). The widget only renders the Material section once at least one of `density`/`swell`/`shrink` is configured — no separate toggle. `stale` flags the brief window between a footprint committing and its elevation sample resolving.
|
|
356
360
|
- `vega-lite`
|
|
361
|
+
- `dynamic-chart` — same Vega-Lite rendering as `vega-lite`, but data-driven by a live DOM event instead of a layer/`ctx.data`: `on="<event-name>"` (required — the event to listen for on `<om-map>`), `series-field="<name>"` (default `series`) reads `event.detail[seriesField]` as the chart's `data.values` and re-embeds on every event where that field is a present array; `width` (fixed, default 280) and `title` work the same as `vega-lite`. The child `<script type="application/json">` spec is the same Vega-Lite mark/encoding shape, minus `data` (supplied live). A feature "freezes" the chart for free by simply not including the field on a later event (e.g. switching modes) — the widget has no separate pause API, it just does nothing when the field is absent. Built for a feature that computes its own series as the user interacts (a drawn line's elevation profile updating vertex-by-vertex) and has no layer of its own to bind to.
|
|
357
362
|
- `player`
|
|
358
363
|
- `basemap-switcher` — radio list of presets; `options="positron dark-matter osm"` (default: every keyless registered preset)
|
|
359
364
|
- `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
365
|
- `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
366
|
- `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.
|
|
367
|
+
- `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.
|
|
368
|
+
|
|
369
|
+
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.
|
|
370
|
+
|
|
371
|
+
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.
|
|
372
|
+
- `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.
|
|
373
|
+
- `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.
|
|
374
|
+
- `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
375
|
|
|
363
376
|
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
377
|
|
|
@@ -416,12 +429,17 @@ watched token fires:
|
|
|
416
429
|
</om-widget>
|
|
417
430
|
```
|
|
418
431
|
|
|
419
|
-
- `ctx.layers
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
-
|
|
423
|
-
|
|
424
|
-
|
|
432
|
+
- `ctx.layers`
|
|
433
|
+
- `ctx.data(id)`
|
|
434
|
+
- `ctx.dataInViewport(id)`
|
|
435
|
+
- `ctx.stats(id, field, { scope: "viewport" })`
|
|
436
|
+
- `ctx.selection`
|
|
437
|
+
- `ctx.viewport`
|
|
438
|
+
- `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.
|
|
439
|
+
- `ctx.history` — `{ canUndo, canRedo }`; re-render on changes via the `history` watch token
|
|
440
|
+
- `ctx.emit(action, payload)`
|
|
441
|
+
|
|
442
|
+
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
443
|
|
|
426
444
|
**Drive the map — the emission contract.** This is the part authors get wrong.
|
|
427
445
|
A widget NEVER mutates the map directly and NEVER dispatches its own
|
|
@@ -481,15 +499,25 @@ Anchors:
|
|
|
481
499
|
- `anchor-from="selection"`
|
|
482
500
|
- `anchor-layer="regions" anchor-feature-id="mission"`
|
|
483
501
|
|
|
502
|
+
Selection scoping (both only apply with `anchor-from="selection"`):
|
|
503
|
+
|
|
504
|
+
- `layer="quakes"` — only that layer's picks move/re-template the overlay.
|
|
505
|
+
- `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.
|
|
506
|
+
|
|
507
|
+
Viewport clipping:
|
|
508
|
+
|
|
509
|
+
- `clip-to-map` — hide the overlay when its own BOX would spill past the map viewport, not just when its anchor leaves (the default). Opt-in, because an overhanging absolutely-positioned box inflates the page's scrollable overflow, and the resulting scrollbar → map resize → reprojection loop shows as view jitter. Use it for small transient tips that track the cursor; an authored popup near an edge normally wants to keep showing its visible half.
|
|
510
|
+
|
|
484
511
|
Templates:
|
|
485
512
|
|
|
486
513
|
- `{{field}}` HTML-escaped interpolation.
|
|
487
514
|
- `{{{field}}}` raw HTML; avoid unless trusted.
|
|
515
|
+
- `{{z}}` — the pick's elevation in meters, present only when a `pickable="3d"` layer ran the depth pass for it (see `<om-layer>`'s `pickable` above). Absent, not `0`, otherwise.
|
|
488
516
|
|
|
489
517
|
Example:
|
|
490
518
|
|
|
491
519
|
```html
|
|
492
|
-
<om-overlay id="detail" anchor-from="selection" visible="false">
|
|
520
|
+
<om-overlay id="detail" anchor-from="selection" selection-type="click" visible="false">
|
|
493
521
|
<div><b>{{place}}</b> M {{magnitude}}</div>
|
|
494
522
|
</om-overlay>
|
|
495
523
|
<om-behavior on="click" layer="quakes" action="show-overlay" target="detail"></om-behavior>
|
|
@@ -540,6 +568,9 @@ Common built-in actions:
|
|
|
540
568
|
- story actions: `story-play`, `story-pause`, `story-seek`
|
|
541
569
|
- effect actions: `fade`, `pulse`, `trace`, `populate`
|
|
542
570
|
- draw actions: `draw-mode`, `draw-commit`, `draw-cancel`, `draw-delete`, `draw-clear`, `draw-config`, `draw-save`
|
|
571
|
+
- measure actions: `measure-mode` (`{mode: "distance"|"area"|"volume"|null}`), `measure-units` (`{units: "metric"|"imperial"}`), `measure-clear`, `measure-config` (`{profile?, baseSurface?, density?, swell?, shrink?, deadband?}`), `measure-flat-target-plane` (`{flat}` — volume mode's target-surface switch)
|
|
572
|
+
- clip box: `set-clip-box` (`{min, max, invert?, highlight?}`, or `{clear: true}` to remove it), `clip-box-edit` (`{editing}` — shows/hides the draggable face gizmos)
|
|
573
|
+
- region export: `export-region-3d` (`{target?: "sketch", format?: "glb"|"b3dm"}`) — clips the drawn footprint's 3D content and downloads it; this is what the `draw` widget's `export-3d` button emits
|
|
543
574
|
|
|
544
575
|
Payload attributes are kebab-case and become camelCase payload keys.
|
|
545
576
|
|
|
@@ -635,3 +666,271 @@ For 3D Tiles LOD/refinement experiments, use:
|
|
|
635
666
|
maximum-memory-usage="256"
|
|
636
667
|
view-distance-scale="0.85"></om-layer>
|
|
637
668
|
```
|
|
669
|
+
|
|
670
|
+
### Per-element (BIM) picking and styling
|
|
671
|
+
|
|
672
|
+
A plain `pickable` `Tile3DLayer` picks a whole TILE. To pick individual
|
|
673
|
+
elements — a wall, a window, one IFC product — add `pick-features`. The pick's
|
|
674
|
+
`selection` then carries `featureId`, `properties` and `class` resolved from the
|
|
675
|
+
tile's own `EXT_mesh_features` + `EXT_structural_metadata`:
|
|
676
|
+
|
|
677
|
+
```html
|
|
678
|
+
<om-layer id="building" type="Tile3DLayer"
|
|
679
|
+
tileset="https://example.com/tileset.json"
|
|
680
|
+
pick-features
|
|
681
|
+
feature-id-property="_FEATURE_ID_0"
|
|
682
|
+
load-options='{"gltf":{"loadBuffers":true,"loadImages":true},"image":{"type":"data"}}'
|
|
683
|
+
pickable></om-layer>
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
`feature-styles` recolours, fades or highlights elements by feature ID — an
|
|
687
|
+
array indexed BY id, each entry `{color: [r,g,b], strength: 0-1, opacity: 0-1}`.
|
|
688
|
+
Set it live (from a widget, a dropdown, a pick handler) and only a small lookup
|
|
689
|
+
texture is re-uploaded; no refetch, no re-tesselation:
|
|
690
|
+
|
|
691
|
+
```html
|
|
692
|
+
<om-layer id="building" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
693
|
+
feature-styles='[{"color":[90,200,255],"strength":0.5},{},{"color":[255,215,130],"strength":0.9}]'></om-layer>
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Use `strength` below 1 to TINT rather than replace — a full-strength colour hides
|
|
697
|
+
the model's own texture entirely.
|
|
698
|
+
|
|
699
|
+
**Isolate / hide / ghost** are declarative, and mirror the vector
|
|
700
|
+
`filter-field` + `filter-categories` pair: name the metadata field once, then
|
|
701
|
+
list values per state. Values are JSON arrays, matched against the tile's own
|
|
702
|
+
property table:
|
|
703
|
+
|
|
704
|
+
```html
|
|
705
|
+
<om-layer id="building" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
706
|
+
feature-filter-field="component"
|
|
707
|
+
hide-features='["Windows","Skylight"]'
|
|
708
|
+
ghost-features='["Wall"]'
|
|
709
|
+
ghost-opacity="0.18"></om-layer>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
- `isolate-features` is exclusive — anything NOT listed is hidden, so it is a
|
|
713
|
+
scope rather than another kind of hide.
|
|
714
|
+
- Hiding is a shader `discard`, so a hidden element also stops being pickable
|
|
715
|
+
and you can select whatever sits behind it.
|
|
716
|
+
- These compose ONTO `feature-styles` rather than replacing it: the style table
|
|
717
|
+
supplies colour, these supply visibility. Changing colour scheme never
|
|
718
|
+
un-hides anything, and isolating never drops your colouring.
|
|
719
|
+
- Being attributes, they are undoable and story-steppable — prefer them over
|
|
720
|
+
computing a `feature-styles` table in page JS.
|
|
721
|
+
|
|
722
|
+
**Colour by property.** `feature-styles` is indexed by feature ID, which means
|
|
723
|
+
computing a table by hand. These name a property-table FIELD instead and build
|
|
724
|
+
that table for you, once the table arrives with the first tile:
|
|
725
|
+
|
|
726
|
+
```html
|
|
727
|
+
<om-layer id="clinic" type="Tile3DLayer" tileset="…" pick-features pickable
|
|
728
|
+
feature-color-by="material"></om-layer>
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
- `feature-color-by` is CATEGORICAL — one palette entry per distinct value.
|
|
732
|
+
`feature-palette='["#4f7cff","#ff9a3c"]'` overrides the built-in
|
|
733
|
+
colour-blind-safe cycle.
|
|
734
|
+
- `feature-color-scale` is GRADUATED over a numeric field.
|
|
735
|
+
- `feature-color-strength` (default 0.85) is how hard the colour is mixed over
|
|
736
|
+
the model's own material; below 1 tints rather than replaces.
|
|
737
|
+
- Setting NEITHER is meaningful, and is the default: the model renders in its
|
|
738
|
+
own IFC surface colours, which is what someone opening a building expects.
|
|
739
|
+
Reach for these to answer a question, not to make it look coloured.
|
|
740
|
+
- An authored `feature-styles` always wins — these are sugar over the same
|
|
741
|
+
table, never an override of it.
|
|
742
|
+
|
|
743
|
+
A graduated ramp needs the field to actually be populated. Revit IFC2x3 exports
|
|
744
|
+
frequently carry no `IfcElementQuantity` at all, so every `netVolume` is 0 and
|
|
745
|
+
the ramp renders flat — check the property table before blaming the ramp.
|
|
746
|
+
|
|
747
|
+
**Georeferencing — `site-origin` / `site-heading` / `site-scale`** (Tile3DLayer
|
|
748
|
+
and PathLayer). Where a model sits is a viewing decision, not a conversion one,
|
|
749
|
+
so it lives on the layer:
|
|
750
|
+
|
|
751
|
+
```html
|
|
752
|
+
<om-layer id="clinic" type="Tile3DLayer" tileset="…/tileset.json" pick-features pickable
|
|
753
|
+
site-origin="[-71.059776, 42.358429]" site-heading="32" site-scale="1"></om-layer>
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
- `site-origin` is `[lng, lat]` or `[lng, lat, elevation]`, and it OVERRIDES the
|
|
757
|
+
position baked into the tileset's root transform rather than offsetting it.
|
|
758
|
+
- `site-heading` is a bearing — degrees CLOCKWISE from true north. On its own,
|
|
759
|
+
with no `site-origin`, it rotates the model where it stands.
|
|
760
|
+
- Rotation and scale pivot on the model's own anchor, not the tileset origin,
|
|
761
|
+
so a heading change spins the building about itself.
|
|
762
|
+
- An IFC model is a PAIR of layers — the mesh tileset and a `PathLayer` outline
|
|
763
|
+
overlay whose paths are local east/north/up metres — and both need the same
|
|
764
|
+
three values, or the building walks away from its own edges. The
|
|
765
|
+
`ifc-loader` widget sets them on both for you.
|
|
766
|
+
- Do not trust a model's declared position without looking at it. Authoring
|
|
767
|
+
tools ship a default project location, and a default is indistinguishable
|
|
768
|
+
from a survey: the buildingSMART Medical-Dental Clinic sample carries Revit's
|
|
769
|
+
Boston default and the Duplex a Chicago city-centre point, so both land on
|
|
770
|
+
occupied downtown blocks at an arbitrary rotation. `IfcMapConversion` (real
|
|
771
|
+
georeferencing) is absent from most IFC2x3 exports and `TrueNorth` is
|
|
772
|
+
routinely unset — which is exactly what these attributes are for.
|
|
773
|
+
- Editing `site-*` on a live Tile3DLayer reloads the tileset (deck.gl only
|
|
774
|
+
reloads on a URL change, so the runtime cache-busts the URL). The outline
|
|
775
|
+
PathLayer updates as a uniform, with no reload.
|
|
776
|
+
|
|
777
|
+
Three constraints worth knowing before promising this to a user:
|
|
778
|
+
|
|
779
|
+
- Multi-material models are fine: glTF allows one material per primitive, so a
|
|
780
|
+
five-material house is five primitives and a real IFC export is often dozens —
|
|
781
|
+
all of them get per-element picking. Only genuinely instanced (i3dm) tiles fall
|
|
782
|
+
back to tile-granularity picking, where `feature-styles` does nothing.
|
|
783
|
+
- Datasets that store IDs in a TEXTURE (photogrammetry classification) need
|
|
784
|
+
`load-options` with `gltf.loadImages`, `gltf.loadBuffers` AND
|
|
785
|
+
`image: {"type": "data"}`. Without the last one the tileset takes minutes to
|
|
786
|
+
appear — loaders.gl otherwise reads the whole ID texture back through a canvas
|
|
787
|
+
once per vertex.
|
|
788
|
+
- `opacity: 0` HIDES an element (a shader discard, so it also stops being
|
|
789
|
+
pickable and you can select what is behind it). Partial `opacity` (ghosting) is
|
|
790
|
+
still being validated.
|
|
791
|
+
|
|
792
|
+
Worked example: `examples/ferry-building-features.html`.
|
|
793
|
+
|
|
794
|
+
### BIMLayer — declarative in-browser loading
|
|
795
|
+
|
|
796
|
+
`<om-layer type="BIMLayer" src="./model.ifc">` is the declarative counterpart
|
|
797
|
+
to `ifc-loader`/`loadIfc`: point it at a BIM source file (an `.ifc` today,
|
|
798
|
+
other formats plug into the same layer later) and it runs the file through
|
|
799
|
+
the loader itself the moment `src` resolves — no pre-baked tileset, no
|
|
800
|
+
`site-origin`/`site-heading`/`site-scale` (the file's own georeference is
|
|
801
|
+
read and applied automatically; the attributes exist to OVERRIDE a wrong or
|
|
802
|
+
missing reading, not to restate what the loader already computed — an
|
|
803
|
+
authored `site-origin` on a BIMLayer is not wired up yet, a documented gap),
|
|
804
|
+
and no separate `<om-layer type="PathLayer">` for the outline overlay (it's
|
|
805
|
+
added automatically). Everything else about a picked BIM layer — `pick-features`
|
|
806
|
+
(defaults ON, unlike a plain `Tile3DLayer`), `feature-filter-field`,
|
|
807
|
+
`feature-styles`, `isolate-features`/`hide-features`/`ghost-features`,
|
|
808
|
+
`feature-color-by`, `ghost-opacity` — works exactly as it does on
|
|
809
|
+
`Tile3DLayer`, because BIMLayer forwards them to a real `Tile3DLayer` it
|
|
810
|
+
builds internally:
|
|
811
|
+
|
|
812
|
+
```html
|
|
813
|
+
<om-layer id="clinic" type="BIMLayer" src="./clinic.ifc"
|
|
814
|
+
label="Medical-Dental Clinic"
|
|
815
|
+
feature-filter-field="ifcClass"
|
|
816
|
+
ghost-opacity="0.15"
|
|
817
|
+
pickable></om-layer>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
Reach for `BIMLayer` when the model is fixed and known ahead of time
|
|
821
|
+
(`dev/examples/features/terrain-3d/inspect-a-bim-model.html`); reach for the `ifc-loader` widget (BIM
|
|
822
|
+
workbench) when a visitor picks the file, or when several models need to
|
|
823
|
+
federate into one coordinated scene. **Known gap:** the outline overlay does
|
|
824
|
+
not yet follow isolate/hide/ghost the way the mesh does — `ifc-browser`'s
|
|
825
|
+
visibility sync targets a separate `<layer-id>-edges` element by convention,
|
|
826
|
+
and BIMLayer's outline never reaches the DOM as one.
|
|
827
|
+
|
|
828
|
+
### In-browser IFC
|
|
829
|
+
|
|
830
|
+
`loadIfc` parses an `.ifc` with web-ifc (WASM) and returns a 3D Tiles model held
|
|
831
|
+
entirely in memory — nothing is uploaded and nothing is written to disk. Because
|
|
832
|
+
the output IS a tileset, `pick-features`, `feature-styles`, `site-*` and the
|
|
833
|
+
declarative isolate/hide/ghost attributes work on it unchanged.
|
|
834
|
+
|
|
835
|
+
Most pages should not call it directly — `<om-widget type="ifc-loader">` owns
|
|
836
|
+
the drop zone, the call, the layer elements, the camera and the blob-URL
|
|
837
|
+
lifetime. `examples/features/terrain-3d/inspect-a-bim-model.html` is the whole workflow — a model on load, a drop zone for more, browse and coordinate — in five widget tags.
|
|
838
|
+
|
|
839
|
+
```js
|
|
840
|
+
import { loadIfc, configureIfc } from "@nika-js/onlymap";
|
|
841
|
+
|
|
842
|
+
configureIfc({ wasmPath: "/vendor/web-ifc/" }); // optional: self-host instead of the CDN
|
|
843
|
+
const arch = await loadIfc(archBytes, { onProgress: (m) => console.log(m) });
|
|
844
|
+
// FEDERATION: pass the first model's origin so the two share a frame.
|
|
845
|
+
const mep = await loadIfc(mepBytes, { origin: arch.origin });
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Returns `tilesetUrl` and `edgesUrl` (blob URLs — assign to a `Tile3DLayer`'s
|
|
849
|
+
`tileset` and a companion `PathLayer`'s `data`), `loadOptions` to pass straight
|
|
850
|
+
through, `features` (the property table, for legends and category lists —
|
|
851
|
+
`ifcClass`, `name`, `material`, `container`, `netVolume`, plus the hierarchy
|
|
852
|
+
columns `spatialPath` / `typePath` / `systemPath` / `classificationPath`, each
|
|
853
|
+
joined by `SPATIAL_SEPARATOR` (U+001F, which cannot occur in an IFC label) and
|
|
854
|
+
emitted only when the file populates it),
|
|
855
|
+
`lonLat`/`georeferenced`/`heading`/`scale`/`originSource`/`headingSource` for
|
|
856
|
+
the `site-*` attributes, `stats`, `timings` (ms per phase), `bounds`, and
|
|
857
|
+
`revoke()`.
|
|
858
|
+
|
|
859
|
+
- **Call `revoke()` when you swap models.** Blob URLs are held by the document
|
|
860
|
+
and are not garbage collected.
|
|
861
|
+
- **`origin` is federation.** Each model is otherwise centred on its own
|
|
862
|
+
bounding box, so two discipline models of one building drift apart by the
|
|
863
|
+
difference between those boxes — and a clash pass then reports nothing, which
|
|
864
|
+
is indistinguishable from a clean model. Pass the first model's `origin` into
|
|
865
|
+
every later `loadIfc` for the same building. `ifc-loader` does this for you
|
|
866
|
+
per map, and shares the PLACEMENT too (`site-origin`/`site-heading`/`site-scale`):
|
|
867
|
+
discipline files routinely declare IfcSite coordinates kilometres apart for the
|
|
868
|
+
same building, so the first model loaded decides where it goes and the rest
|
|
869
|
+
follow. `independent` opts out.
|
|
870
|
+
- Every element also gets a bounding box in the property table
|
|
871
|
+
(`bboxMinE`/`bboxMinN`/`bboxMinU`/`bboxMaxE`/`bboxMaxN`/`bboxMaxU`, tile-local
|
|
872
|
+
metres), which is what `ifc-clash` reads.
|
|
873
|
+
- web-ifc is MPL-2.0 and is NOT a package dependency — it is dynamic-imported
|
|
874
|
+
from unpkg on first use, so it never reaches the bundle and pages that never
|
|
875
|
+
open an IFC pay nothing. Measured: the IFC loader chunk is 23.7 KB raw /
|
|
876
|
+
8.5 KB gzipped and contains only the CDN URL; no web-ifc code and no `.wasm`
|
|
877
|
+
ship in `dist/`.
|
|
878
|
+
- **For OFFLINE, air-gapped or strict-CSP deployments, self-host it.**
|
|
879
|
+
`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),
|
|
880
|
+
then either `wasm-path="/vendor/web-ifc/"` on the `ifc-loader` widget (no
|
|
881
|
+
script needed) or `configureIfc({wasmPath})` once before the first model
|
|
882
|
+
loads. web-ifc is only ONE of the network dependencies though: a georeferenced
|
|
883
|
+
model makes `ifc-loader` switch a basemap on, which reaches a tile server, so
|
|
884
|
+
offline pages also want `basemap="none"` on the loader and `telemetry="off"`
|
|
885
|
+
on the map. They stay STATIC ASSETS fetched by the same lazy import — the JS
|
|
886
|
+
bundle does not grow. Cost: `web-ifc-api.js` 5.31 MB raw / 0.49 MB gzipped
|
|
887
|
+
plus `web-ifc.wasm` 1.20 MB / 0.44 MB, so ~0.93 MB gzipped, served once and
|
|
888
|
+
cached. (`web-ifc-mt.wasm` is a further 1.22 MB and is only needed for the
|
|
889
|
+
multithreaded path.) Note a dynamic import cannot carry subresource
|
|
890
|
+
integrity, so the pinned version in the URL is the only thing fixing what
|
|
891
|
+
runs — another reason to serve it yourself.
|
|
892
|
+
- Position is read from the file, preferring the trustworthy route.
|
|
893
|
+
`IfcMapConversion` — a surveyed placement into a named projected CRS — wins
|
|
894
|
+
over `IfcSite.RefLatitude`/`RefLongitude`, which is very often an authoring
|
|
895
|
+
default. `originSource` says which was used: `"map-conversion"`, `"ifc-site"`
|
|
896
|
+
or `"fallback"`.
|
|
897
|
+
- **Anything other than `"map-conversion"` raises a structured `"warning"`**
|
|
898
|
+
through the same validation channel other `om-layer` errors use (visible in
|
|
899
|
+
the on-page panel with `validate` set, and on `om-validation-error`'s
|
|
900
|
+
`detail.warnings`) — for both `BIMLayer` and the `ifc-loader` widget, once
|
|
901
|
+
per layer. It does not flip `valid` to `false`; it flags that the position
|
|
902
|
+
may be off by tens of metres with no rotation correction applied. Override
|
|
903
|
+
with `site-origin`/`site-heading` once the real location is known, or
|
|
904
|
+
re-export the model with a proper `IfcMapConversion`.
|
|
905
|
+
- Un-projecting a map conversion covers WGS84 UTM zones analytically, plus
|
|
906
|
+
every CRS in the bundled table `src/crs.ts` shares with the CityJSON
|
|
907
|
+
decoder (Dutch RD, Swiss LV95, ETRS89/UTM, Japan's plane systems, …).
|
|
908
|
+
Anything else is declined with a warning naming the bundled codes, rather
|
|
909
|
+
than approximated — a guessed projection lands the model in another
|
|
910
|
+
country while looking entirely plausible.
|
|
911
|
+
- A model aligned to a NATIONAL GRID is not aligned to true north, so the
|
|
912
|
+
grid convergence is measured and folded into the heading. Dutch RD at
|
|
913
|
+
Rotterdam leans -0.735 degrees, and it grows with distance from the
|
|
914
|
+
central meridian.
|
|
915
|
+
- The conversion is APPLIED, not just read. It anchors the model's ORIGIN,
|
|
916
|
+
while the tileset is recentred on its geometry, so the full affine
|
|
917
|
+
(offset, grid axis, scale) is applied to the anchor point. Skipping that
|
|
918
|
+
put one Revit export 32.7 m out, its survey point being that far from the
|
|
919
|
+
building.
|
|
920
|
+
- `IfcMapConversion` OUTRANKS `IfcSite` for position. Files carrying both
|
|
921
|
+
routinely disagree: one Dutch model's two statements are 108 m apart, and
|
|
922
|
+
the projected pair is the surveyed one. Note the eastings/northings are in the target CRS's own
|
|
923
|
+
unit, which is frequently MILLIMETRES.
|
|
924
|
+
- `headingSource` distinguishes `"map-conversion"` / `"true-north"` (read from
|
|
925
|
+
the file) from `"assumed"` (the file was silent and project north was taken as
|
|
926
|
+
true north). Report the assumption; do not let it read as a measurement.
|
|
927
|
+
- A file can be perfectly georeferenced and still land somewhere useless: all
|
|
928
|
+
three prepared samples in this repo declare placeholder positions (the clinic
|
|
929
|
+
on Revit's Boston default, which reverse-geocodes to a 1630 graveyard; the
|
|
930
|
+
duplex on a Chicago city-centre point; the bridge, which DOES carry a real
|
|
931
|
+
`IfcMapConversion`, into the mid-Pacific at 179.08E 8.46S). Reading the file
|
|
932
|
+
correctly and the model being in a sensible place are separate problems, and
|
|
933
|
+
`site-origin` is the fix for the second.
|
|
934
|
+
- `<om-map>` reads its camera attributes ONCE at init, so `setAttribute("center", …)`
|
|
935
|
+
after mount moves nothing and leaves the model outside the frustum. Use
|
|
936
|
+
`map.flyTo(lonLat, zoom)`.
|