@nika-js/onlymap 0.6.1 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +106 -1
  2. package/README.md +28 -15
  3. package/dist/{LercDecode.es-CJnypw8j.js → LercDecode.es-D5in29tf.js} +1 -1
  4. package/dist/{basemap-DrQ0-eyR.js → basemap-C0pFT3AO.js} +13 -3
  5. package/dist/basemap.d.ts +10 -1
  6. package/dist/clip-box-controller.d.ts +94 -0
  7. package/dist/clip-box.d.ts +109 -0
  8. package/dist/data-layer.d.ts +34 -4
  9. package/dist/draw-controller.d.ts +75 -1
  10. package/dist/draw.d.ts +22 -4
  11. package/dist/elements/om-map.d.ts +20 -0
  12. package/dist/elements/om-overlay.d.ts +12 -0
  13. package/dist/geodesy.d.ts +32 -3
  14. package/dist/{geoparquet-Dix4lTNy.js → geoparquet-By98JVB0.js} +1 -1
  15. package/dist/html-data.d.ts +2 -2
  16. package/dist/{index-BQMjW5w0.js → index-Bx9GFkrn.js} +1 -1
  17. package/dist/{index-CXPaeisL.js → index-CqC4sW_k.js} +1 -1
  18. package/dist/{index-1UgNlfGR.js → index-Cxo9mCw_.js} +30947 -27785
  19. package/dist/{index-B_1PPJgC.js → index-olfncHIq.js} +1 -1
  20. package/dist/{index-CAuT5j9Y.js → index-tnlYDALL.js} +2 -2
  21. package/dist/index.d.ts +8 -3
  22. package/dist/ir-snapshot.d.ts +3 -1
  23. package/dist/layers/bim-layer.d.ts +30 -7
  24. package/dist/layers/feature-mesh-layer.d.ts +1 -1
  25. package/dist/layers/popup-layer.d.ts +13 -0
  26. package/dist/legend-spec.d.ts +1 -1
  27. package/dist/{lerc-CbTjQ7uI.js → lerc-gKDDtc69.js} +2 -2
  28. package/dist/license.d.ts +25 -4
  29. package/dist/measure-controller.d.ts +482 -4
  30. package/dist/onlymap.standalone.js +53976 -50804
  31. package/dist/onlymapjs.js +42 -40
  32. package/dist/parse-manifest.d.ts +3 -0
  33. package/dist/programmatic.d.ts +34 -5
  34. package/dist/{raster-0b0nSHUh.js → raster-Cl5m3KsC.js} +2 -2
  35. package/dist/{raster-pipeline-D8siq7-4.js → raster-pipeline-hJGxIwYx.js} +1 -1
  36. package/dist/react/om-layer.d.ts +6 -1
  37. package/dist/react.js +167 -160
  38. package/dist/region-export-controller.d.ts +37 -0
  39. package/dist/region-export.d.ts +56 -0
  40. package/dist/runtime-core.d.ts +150 -0
  41. package/dist/selection.d.ts +11 -1
  42. package/dist/site-placement.d.ts +22 -0
  43. package/dist/snapping.d.ts +88 -0
  44. package/dist/terrain-heightfield.d.ts +53 -0
  45. package/dist/terrain-sample.d.ts +30 -0
  46. package/dist/terrain.d.ts +6 -1
  47. package/dist/units.d.ts +27 -0
  48. package/dist/version.d.ts +1 -1
  49. package/dist/volumetrics-run.d.ts +13 -0
  50. package/dist/volumetrics-worker.d.ts +1 -0
  51. package/dist/volumetrics.d.ts +201 -0
  52. package/dist/{zarr-DCYro_Vs.js → zarr-hRDavRGV.js} +2 -2
  53. package/docs/3d-assets.md +49 -1
  54. package/docs/live-data.md +26 -1
  55. package/docs/react.md +9 -1
  56. package/docs/testing.md +2 -0
  57. package/llms.txt +10 -5
  58. package/onlymapjs.html-data.json +161 -2
  59. package/package.json +4 -2
  60. package/skills/onlymapjs/SKILL.md +11 -1
  61. package/skills/onlymapjs/references/react.md +2 -2
  62. package/skills/onlymapjs/references/syntax.md +20 -8
  63. package/skills/onlymapjs/references/testing.md +6 -0
@@ -1,6 +1,40 @@
1
+ /**
2
+ * Measure controller (spec: issue #20 "Measure widget — geodesic distance and
3
+ * area"). Capture is entirely the existing draw stack: it drives the shared
4
+ * DrawController on the runtime-internal target `__onlymapjs-measure`, so the
5
+ * one active-tool slot gives measure↔draw mutual exclusion for free, and the
6
+ * internal prefix already exempts the geometry from ctx.layers, license gates,
7
+ * telemetry, undo history, and save. This module adds only the geodesic math
8
+ * hookup (via a DrawController change observer), the live badge labels (an
9
+ * internal PopupLayer on the per-frame channel), and the `om-measure` readout
10
+ * event the widget renders. Owned per <om-map> (the draw/tooltip precedent),
11
+ * driven by `measure-*` actions.
12
+ */
13
+ import type { PickingInfo } from "@deck.gl/core";
1
14
  import { type UnitSystem } from "./units";
2
- import type { RuntimeCore } from "./runtime-core";
3
- export type MeasureMode = "distance" | "area";
15
+ import { type BaseSurfaceKind } from "./volumetrics";
16
+ import type { RuntimeCore, GizmoDragEvent } from "./runtime-core";
17
+ export type MeasureMode = "distance" | "area" | "volume";
18
+ /**
19
+ * One elevation-profile sample on the `om-measure` readout.
20
+ *
21
+ * `vertexIndex` is present ONLY on samples that ARE one of the drawn
22
+ * footprint's own vertices (0-based, in draw order; the closing sample back at
23
+ * the start carries the last index). Interpolated samples omit it entirely, so
24
+ * a Vega-Lite chart marks the real corners with `isValid(datum.vertexIndex)` —
25
+ * see `dev/examples/features/widgets/measure-cut-fill-volume.html`. Without
26
+ * this a reader cannot tell WHICH point on the map a place in the chart
27
+ * corresponds to; vertex 0 is the leftmost sample, and the measure tool marks
28
+ * that same point on the map with a "Start" badge.
29
+ */
30
+ export interface ProfilePoint {
31
+ /** Distance along the footprint's perimeter from the first vertex, in meters. */
32
+ x: number;
33
+ /** Terrain elevation at this sample, in meters. */
34
+ y: number;
35
+ /** 0-based index into the drawn footprint's vertices — present only on real vertices. */
36
+ vertexIndex?: number;
37
+ }
4
38
  /** Live readout dispatched as the `om-measure` event `detail` and consumed by the widget. */
5
39
  export interface MeasureReadout {
6
40
  mode: MeasureMode | null;
@@ -13,28 +47,472 @@ export interface MeasureReadout {
13
47
  perimeterMeters: number;
14
48
  /** area readout is unavailable because the ring winds around a pole (documented limitation). */
15
49
  poleWarning: boolean;
50
+ /**
51
+ * volume mode only (spec: "Cut/fill volume measurement"). `null` before the
52
+ * footprint is closed — cut/fill/net are meaningless without a base
53
+ * elevation. RAW geometric volumes (ground truth: "does this reach target
54
+ * elevation") — deliberately UNAFFECTED by `swell`/`shrink` (see
55
+ * `cutAdjustedMeters3`/`fillAdjustedMeters3` for the material-adjusted
56
+ * figures) so this pair never silently changes meaning depending on
57
+ * whether a density/factor attribute happens to be set. v1 samples ONE
58
+ * ground elevation at the footprint's centroid and treats it as flat: cut
59
+ * and fill are mutually exclusive per polygon (dragging the gizmo up is
60
+ * pure fill, down is pure cut) — real per-cell terrain-relative
61
+ * integration on sloped ground is a documented follow-up.
62
+ */
63
+ cutMeters3?: number | null;
64
+ fillMeters3?: number | null;
65
+ /**
66
+ * volume mode only. SIGNED, from the RAW (not material-adjusted) cut/fill:
67
+ * `fillMeters3 − cutMeters3`. Positive = net material added.
68
+ */
69
+ netMeters3?: number | null;
70
+ /**
71
+ * volume mode only (spec item D — "Reporting, units, factors"). Unsigned
72
+ * RAW `cutMeters3 + fillMeters3` — distinct from the SIGNED `netMeters3`:
73
+ * cut and fill are mutually exclusive per polygon in this v1 (dragging is
74
+ * one direction at a time), so this only ever equals whichever of cut/fill
75
+ * is currently nonzero, but it's a separate field so a "Total moved"
76
+ * readout doesn't have to infer that from the sign of net — same "both
77
+ * conventions labelled unambiguously" ask that motivates `netMeters3`'s
78
+ * own sign.
79
+ */
80
+ totalMeters3?: number | null;
81
+ /**
82
+ * volume mode only (spec item D). Material-adjusted volumes, standard
83
+ * Bank/Loose/Compacted earthworks convention: `cutAdjustedMeters3` = raw
84
+ * cut × `swell` (bank → loose/haul volume — excavating adds air voids, so
85
+ * this is BIGGER than `cutMeters3`); `fillAdjustedMeters3` = raw fill ÷
86
+ * `shrink` (raw fill is already a compacted target-void volume; dividing
87
+ * by a <1 shrink factor gives the BIGGER loose/borrow volume actually
88
+ * needed on-site to achieve it after compaction settles it down). Each is
89
+ * `null` unless its own factor was actually configured away from the
90
+ * neutral default (1×) — the widget renders the "Material" section only
91
+ * when at least one of these (or `cutMassKg`/`fillMassKg`) is non-null, no
92
+ * separate show/hide toggle.
93
+ */
94
+ cutAdjustedMeters3?: number | null;
95
+ fillAdjustedMeters3?: number | null;
96
+ /** volume mode only. The `swell`/`shrink` multipliers currently in effect (default 1×) — for labeling the Material section's rows with the actual factor applied, not just the adjusted number. */
97
+ swell?: number;
98
+ shrink?: number;
99
+ /**
100
+ * volume mode only (spec item D). Mass in kg — canonical regardless of
101
+ * unit system, format with `formatMass` at render time. Computed from the
102
+ * RAW (not material-adjusted) cut/fill volume: swell/shrink change VOLUME
103
+ * via added/removed air voids, not the actual mass of material, so basing
104
+ * tonnage on the raw figure is the only mass-conserving choice — a second
105
+ * "adjusted tonnage" would misrepresent the same dirt as weighing a
106
+ * different amount depending on how it's packed. `null` both before a
107
+ * footprint closes AND whenever the widget's `density` attribute was never
108
+ * set (same "not applicable" convention as `cutMeters3`; the widget only
109
+ * renders a tonnage row when this is non-null).
110
+ */
111
+ cutMassKg?: number | null;
112
+ fillMassKg?: number | null;
113
+ /**
114
+ * volume mode only (spec: issue #35 — the real per-cell grid integration).
115
+ * Populated ONLY once the grid integrator has run — i.e. terrain is
116
+ * active, the footprint has closed, and the covering DEM tiles resolved;
117
+ * `null` before that (and forever, on a no-terrain page, where the flat
118
+ * fallback math is all there is). When these are non-null, `cutMeters3`/
119
+ * `fillMeters3` above ARE the integrator's per-cell results — mixed cut
120
+ * AND fill within one footprint on undulating ground, not the flat
121
+ * one-or-the-other approximation — and these fields carry the published
122
+ * error model the numbers were computed under:
123
+ * - `cellSizeM`: the grid cell size actually used (≥ `gsdM` when the cell
124
+ * budget forced coarsening — never silently finer than the data).
125
+ * - `gsdM`: the DEM's native ground-sample distance at the ring's
126
+ * latitude — what the ±error is quoted against.
127
+ * - `cutErrorM3`/`fillErrorM3`: ± bounds, per-cell `cellArea × 1.5 × gsd`
128
+ * summed on whichever side each cell landed (the spec's exact formula).
129
+ * - `nodataFraction`: the share of inside-the-ring cells skipped for
130
+ * missing DEM data — a consumer should distrust the totals when this is
131
+ * material.
132
+ */
133
+ cellSizeM?: number | null;
134
+ gsdM?: number | null;
135
+ cutErrorM3?: number | null;
136
+ fillErrorM3?: number | null;
137
+ nodataFraction?: number | null;
138
+ /**
139
+ * volume mode only. Which base-surface strategy produced the cut/fill
140
+ * numbers (widget `base-surface` attribute; default `custom` — the
141
+ * gizmo's draggable target plane). Always present in volume mode, even
142
+ * while the integrator hasn't resolved (the flat fallback is a `custom`
143
+ * plane by construction).
144
+ */
145
+ baseSurface?: BaseSurfaceKind;
146
+ /**
147
+ * volume mode + `base-surface="custom"` only (the gizmo-draggable target
148
+ * plane — the other strategies have no gizmo to toggle). Switches which
149
+ * target SURFACE the footprint is graded to, math and rendering together:
150
+ * `false` (default) — the terrain itself offset by the dragged distance
151
+ * (a slope-parallel prism; uniform depth everywhere, so volume =
152
+ * |offset| × area, pure cut OR pure fill, exactly zero at rest); `true` —
153
+ * one genuinely level plane at centroid elevation + offset (mixed cut AND
154
+ * fill on sloped ground, per-cell integrated). The drawn footprint ring
155
+ * itself always stays on real terrain in both — the toggle only moves the
156
+ * TARGET face. Reported here so the widget's toggle button can reflect
157
+ * the controller's own truth rather than tracking a shadow copy of it.
158
+ */
159
+ flatTargetPlane?: boolean;
160
+ /**
161
+ * volume mode only (spec item D). True while the currently-dispatched
162
+ * cut/fill/net/total numbers reflect a PRIOR footprint/elevation sample,
163
+ * not the one now in effect — set the instant a footprint commits (before
164
+ * its one-shot terrain fetch resolves) and cleared once that fetch
165
+ * completes and a fresh readout is dispatched. Distinct from
166
+ * `footprintClosed`'s "sampling elevation…" case (no numbers exist yet at
167
+ * all): `stale` covers the narrower "numbers exist, but they're for a
168
+ * footprint/base-elevation that no longer applies" window, e.g. right
169
+ * after committing a SECOND footprint while the first's numbers are still
170
+ * the last thing dispatched.
171
+ */
172
+ stale?: boolean;
173
+ /**
174
+ * volume mode only. Disambiguates the two states that otherwise look
175
+ * identical from the numbers alone (segments >= 3, areaMeters2 set,
176
+ * cutMeters3 still null): still sketching a 3+-vertex footprint that
177
+ * hasn't been double-clicked closed yet, vs. just closed and waiting on
178
+ * the one-shot terrain elevation sample to resolve. The widget uses this
179
+ * to avoid claiming "sampling elevation…" while the user is still
180
+ * dragging the rubber-band preview around.
181
+ */
182
+ footprintClosed?: boolean;
183
+ /**
184
+ * volume mode only, and only when the measure widget's `profile` attribute
185
+ * is set (spec item E — "Elevation profile"; not a mode of its own — see
186
+ * `setProfileEnabled`'s doc comment). `PROFILE_SAMPLE_COUNT` evenly-spaced
187
+ * `{x: distance-from-start meters, y: elevation meters}` points around the
188
+ * CLOSED footprint's own perimeter, sampled once alongside the existing
189
+ * per-vertex elevation fetch when the footprint closes — not resampled on
190
+ * drag, since dragging only changes `heightOffsetM`, not the ground ring.
191
+ * `null` (the same "not applicable yet" convention as `cutMeters3`/
192
+ * `fillMeters3`) before a footprint has closed, or whenever `profile`
193
+ * isn't set; absent entirely (not even `null`) in `distance`/`area` mode,
194
+ * whose own dispatches never set this field at all. Either way, a
195
+ * `dynamic-chart` widget reading it sees "not a real array" and leaves
196
+ * whatever it last rendered alone — that's the whole "freeze" mechanism.
197
+ */
198
+ profileSeries?: ProfilePoint[] | null;
16
199
  }
17
200
  export declare class MeasureController {
18
201
  private readonly mapEl;
19
202
  private readonly core;
20
203
  private mode;
21
204
  private units;
205
+ private baseElevationM;
206
+ private heightOffsetM;
207
+ private lastCommittedCount;
208
+ private committedRing;
209
+ /**
210
+ * One terrain elevation sample per `committedRing` vertex (same order),
211
+ * used for the VISUAL geometry (the extruded prism's base ring + the cut
212
+ * guide lines) so the footprint's corners hug the real ground even where
213
+ * it differs from `baseElevationM`'s single centroid sample. The cut/fill
214
+ * VOLUME MATH still deliberately uses that one flat `baseElevationM`
215
+ * delta (documented v1 simplification, unaffected) — this only fixes the
216
+ * PICTURE: reported as "certain sides of the base floating above the
217
+ * terrain rather than draping onto it," which is the expected result of
218
+ * rendering a perfectly flat plane under a footprint whose corners sit at
219
+ * genuinely different real elevations away from the one sampled point.
220
+ */
221
+ private vertexElevationsM;
222
+ /**
223
+ * The committed ring's perimeter DENSIFIED to `GROUND_RING_SAMPLES` points,
224
+ * each with its own terrain sample — the ground-facing edge of everything
225
+ * rendered (extruded base ring, flat-mode wall bottoms). Corner-only rings
226
+ * rendered straight wall bottoms across valleys (visible daylight between
227
+ * the prism and the ground). Sampled once per commit in the SAME
228
+ * heightfield batch as the vertex/centroid/profile points. The footprint
229
+ * INTERIOR ground face is the terrain-draped VOLUME_DRAPE_ID layer, which
230
+ * conforms exactly by construction. Null before terrain resolves — the
231
+ * drawn corners stand in.
232
+ */
233
+ private densifiedGroundRing;
234
+ /** Drag-invariant visual geometry derived from the current ground ring (`buildVolumeVisual` runs per drag frame; stable `data` identity lets deck skip re-parsing/re-draping the unchanged footprint). Keyed by the `ground` array's identity. */
235
+ private volumeVisualCache;
236
+ /** Whether VOLUME_FLAT_ID currently holds faces — an empty-clear of an already-empty layer still costs a full patch/rebuild, and `buildVolumeVisual` runs per drag frame on the (default) non-flat path. */
237
+ private flatFacesLive;
238
+ private centroidLngLat;
239
+ private gizmoDrag;
240
+ /**
241
+ * "Flat target plane" toggle (a widget button, not an authored attribute
242
+ * — a live per-interaction preference, same category as the clip-box
243
+ * gizmo's own `editing` flag, not something a page author configures
244
+ * upfront). See `MeasureReadout.flatTargetPlane`'s own doc comment: it
245
+ * switches the target-surface semantics — parallel-offset vs level plane
246
+ * — so both the integration and the rendered prism change with it.
247
+ */
248
+ private flatTargetPlane;
249
+ /**
250
+ * measure-config action, set from the widget's own `profile` attribute
251
+ * (mirrors `draw-config`'s autosave — a widget-level SETTING, not tied to
252
+ * mode-switching). Deliberately NOT a fourth measure mode: there's no
253
+ * separate line to draw — the profile is always the boundary of whatever
254
+ * footprint volume mode's own polygon already is, resampled live as
255
+ * vertices are added while still sketching and once more, authoritatively,
256
+ * on commit (see `recomputeVolume`/`resampleLivePreviewProfile` and
257
+ * `onFootprintCommitted`). Persists across mode switches (like autosave)
258
+ * rather than resetting — it's a standing "when you draw/close a volume
259
+ * footprint, also do this" preference.
260
+ */
261
+ private profileEnabled;
262
+ private profileSeries;
263
+ /** The vertex count `profileSeries` was last live-resampled at while sketching — gates resampling on a REAL vertex being added (a click), not on hover, since it touches the network. Unrelated to `lastCommittedCount`, which gates the separate final-commit sample. */
264
+ private lastProfileVertexCount;
265
+ /** Generation token — a live-preview resample that resolves AFTER a newer vertex has already superseded it must not overwrite `profileSeries` with stale (shorter-path) data. Two clicks close enough together for their tile fetches to overlap and resolve out of order is the exact scenario this guards. */
266
+ private profileFetchGeneration;
267
+ /** Canonical kg/m³ (converted from the author's t/m³ or lb/yd³ at set-time — see `densityToKgM3`); `null` = tonnage not reported. */
268
+ private densityKgM3;
269
+ /** Bulking multiplier applied to CUT (excavated material occupies more volume loose than in situ). */
270
+ private swell;
271
+ /** Compaction multiplier applied to FILL (placed/compacted material occupies less volume than loose). */
272
+ private shrink;
273
+ /** Cut/fill below this volume (m³, post swell/shrink) reports as zero — filters drag noise/float jitter near h=0 from reading as a nonzero result. */
274
+ private deadbandM3;
275
+ /** True while the dispatched cut/fill/net/total reflect a stale (pre-fetch or prior-footprint) elevation sample — see `MeasureReadout.stale`'s doc comment. */
276
+ private volumeStale;
277
+ /** Base-surface strategy (widget `base-surface` attribute; `custom` = the gizmo's draggable target plane, the v1-compatible default). */
278
+ private baseSurface;
279
+ /** The latest grid-integration result — `null` until the integrator has run for the CURRENT footprint (no terrain, still in flight, or it failed → the flat fallback math stands in). */
280
+ private volResult;
281
+ /** The cached per-cell grid (custom base only) — gizmo drags re-sum it synchronously via `reintegrateCustomBase` instead of re-running the whole pipeline. */
282
+ private volGrid;
283
+ /** Bounds heightfield + boundary samples cached per footprint, so a `base-surface` switch re-integrates without re-fetching tiles. */
284
+ private volHf;
285
+ private volBoundary;
286
+ /** Generation token — an integration resolving AFTER the footprint it was started for is gone (cleared, replaced, mode-switched) must not clobber the new state. */
287
+ private volGeneration;
22
288
  constructor(mapEl: Element, core: RuntimeCore);
289
+ /** measure-config action. */
290
+ setProfileEnabled(enabled: boolean): void;
291
+ /**
292
+ * measure-config action (spec item D). Each field left `undefined` keeps
293
+ * its current value — the widget always emits all four together, but a
294
+ * future caller updating just one factor shouldn't have to resend the
295
+ * others. `density` is interpreted in whatever unit system is ACTIVE right
296
+ * now (`this.units`) and canonicalized to kg/m³ immediately, so a later
297
+ * units toggle doesn't reinterpret it (see `densityToKgM3`'s doc comment).
298
+ * Non-finite or non-positive `density`/`swell`/`shrink` are ignored (fall
299
+ * back to "not set"/1×) rather than propagating NaN/0 into every volume
300
+ * number; a non-finite or negative `deadband` falls back to 0 (no deadband).
301
+ */
302
+ setVolumeFactors(opts: {
303
+ density?: number;
304
+ swell?: number;
305
+ shrink?: number;
306
+ deadband?: number;
307
+ }): void;
308
+ /**
309
+ * measure-config action (spec: issue #35 — "base-surface picker"). A
310
+ * widget-level setting like the factors above. Switching strategies on an
311
+ * ALREADY-COMMITTED footprint re-integrates against the cached heightfield
312
+ * and boundary samples — no tile re-fetch — and flags the readout stale
313
+ * for the (short) in-flight window. `custom` is the gizmo's draggable
314
+ * target plane; every other strategy derives the base from the footprint's
315
+ * own boundary, so the gizmo disappears (there's no plane to drag).
316
+ */
317
+ setBaseSurface(kind: BaseSurfaceKind): void;
23
318
  isActive(): boolean;
24
- /** measure-mode action. `distance`→line capture, `area`→polygon capture, `null`→off. */
319
+ /** measure-mode action. `distance`→line capture, `area`/`volume`→polygon capture, `null`→off. */
25
320
  setMode(mode: MeasureMode | null): void;
321
+ /**
322
+ * Reset volume-tool state (spec: "Cut/fill volume measurement") — on mode
323
+ * change, clear, or teardown. `lastCommittedCount` baselines against the
324
+ * session's ACTUAL current feature count, not 0: distance/area/volume all
325
+ * share one DrawSession (same MEASURE_TARGET), and a mode switch never
326
+ * clears its committed features (only `pending`/`cursor` reset — see
327
+ * draw.ts's own `setMode`). Baselining at 0 made entering volume mode with
328
+ * ANYTHING already committed (a shape drawn in area mode first, or a prior
329
+ * volume attempt) misread as "just committed" on the very next recompute —
330
+ * building the extrusion/gizmo from stale leftover geometry before the
331
+ * user had drawn anything new. `featureCount` reads the session directly
332
+ * (not `getDrawData`'s shared cross-map mirror, which can itself carry a
333
+ * DIFFERENT map's leftover data under the same target id).
334
+ */
335
+ private resetVolumeState;
336
+ /**
337
+ * measure-flat-target-plane action (the widget's own toggle button — see
338
+ * `flatTargetPlane`'s field doc comment). The toggle changes the target
339
+ * SURFACE the prism is graded to — parallel-to-terrain offset vs one level
340
+ * plane — so it re-integrates the cached grid AND re-renders: two
341
+ * different shapes report two different volumes.
342
+ */
343
+ setFlatTargetPlane(flat: boolean): void;
344
+ /**
345
+ * Re-sums the cached per-cell grid under the CURRENT target semantics —
346
+ * `flatTargetPlane` picks which surface the terrain is differenced
347
+ * against: one level plane at centroid + offset (`reintegrateCustomBase`),
348
+ * or the terrain itself shifted by the offset (`reintegrateParallelOffset`,
349
+ * the default — matching the default-rendered slope-parallel prism, and
350
+ * yielding exactly zero at h=0 instead of "grade everything to the
351
+ * centroid's elevation"). Synchronous arithmetic over the cached samples —
352
+ * no re-sampling, no worker round-trip — so it's safe on every drag frame
353
+ * and on the toggle itself.
354
+ */
355
+ private resumCachedGrid;
26
356
  /** measure-units action. Re-render labels + readout in the new system (from the committed shape). */
27
357
  setUnits(units: UnitSystem): void;
28
- /** measure-clear action. Drop the geometry and reset the readout; tool stays selected. */
358
+ /**
359
+ * measure-clear action. Drop the geometry and reset the readout; tool
360
+ * stays selected. `resetVolumeState()` must run AFTER `draw.clear()` (not
361
+ * before) — it reads the draw controller's own feature count as the new
362
+ * `lastCommittedCount` baseline, which is only correct once the clear has
363
+ * actually happened. But `draw.clear()`'s own observer fires FIRST, with
364
+ * `profileSeries` (and the rest of the volume-mode fields) still at their
365
+ * PRE-clear values — previously the only reset was on a MODE SWITCH, so a
366
+ * Clear left the elevation-profile chart showing the just-deleted
367
+ * footprint's stale data forever. The explicit `recompute()` below is a
368
+ * second, corrected dispatch that supersedes that stale one.
369
+ */
29
370
  clear(): void;
371
+ /**
372
+ * Forwarded from om-map's own `onViewportChange` (any camera move, not
373
+ * just gizmo drags/commits) — the gizmo's pixel-locked size needs to
374
+ * re-derive its world-space meters on every zoom change, not only when
375
+ * something ELSE triggers a `buildVolumeVisual` call, or it'd only snap to
376
+ * the correct size the next time the user happens to drag or redraw.
377
+ */
378
+ onViewportChanged(): void;
30
379
  private recompute;
31
380
  /** The path/ring currently being measured: the in-progress preview (+ live cursor), else the last committed shape. */
32
381
  private activeCoords;
382
+ /** Shared by `distance` and `profile` — both are just a line measured live as vertices are added; profile additionally resamples elevation along it. */
383
+ private lineLabelsAndTotal;
33
384
  private recomputeDistance;
34
385
  private recomputeArea;
386
+ /**
387
+ * Detects the footprint's close (a feature-count diff against THIS map's
388
+ * own DrawSession — see DrawSession.count's doc comment for why that beats
389
+ * the shared `getDrawData` mirror — rather than a new DrawController event
390
+ * kind, matching how `activeCoords` already duplicates a bit of logic
391
+ * rather than asking DrawController for something new) and kicks off the
392
+ * one-shot elevation sample. While still sketching, area/perimeter update
393
+ * live exactly like area mode; cut/fill/net stay null until closed.
394
+ */
395
+ private recomputeVolume;
396
+ /**
397
+ * The async half of the live-preview path above — mirrors
398
+ * `onFootprintCommitted`'s own sampling but against the CURRENT (still
399
+ * open) preview PATH, left open (not closed back to the first vertex) —
400
+ * closing only makes sense once there's an actual footprint, which is
401
+ * `onFootprintCommitted`'s job. With 1 vertex and no cursor yet this is a
402
+ * single point at distance 0 (`resamplePath`'s own documented behavior for
403
+ * a 1-point input); with 2+ it's a real, growing line. Re-dispatches off
404
+ * the SAME `coords` snapshot passed in (not a fresh `recompute()`, which
405
+ * needs a live `DrawSession` this method doesn't have) — if the user has
406
+ * moved on by the time this resolves, the vertex-count gate above already
407
+ * queued a fresh call for that, or the shape has since closed and
408
+ * `onFootprintCommitted` owns the readout instead (guarded by the mode
409
+ * check below either way).
410
+ */
411
+ private resampleLivePreviewProfile;
412
+ private onFootprintCommitted;
413
+ /**
414
+ * Fetch the covering DEM tile set for the committed footprint's bbox,
415
+ * sample the boundary, and run the grid integrator (worker-offloaded via
416
+ * `runVolumetrics`; the pure fallback computes identically). Results land
417
+ * in `volResult`/`volGrid` unless the footprint changed underneath the
418
+ * async work (generation token) — the flat v1 math remains the standing
419
+ * fallback whenever this leaves `volResult` null (no terrain, fetch
420
+ * failure, degenerate ring).
421
+ */
422
+ private runVolumeIntegration;
423
+ /**
424
+ * Cut/fill/net/total + material-adjusted volumes + tonnage (spec item D —
425
+ * "Reporting, units, factors"). The RAW cut/fill come from the per-cell
426
+ * grid integrator (`volResult` — spec: issue #35) whenever it has resolved
427
+ * for the current footprint: real terrain-relative integration, mixed cut
428
+ * AND fill within one polygon on undulating ground, with the published
429
+ * error model riding along. Before it resolves — or forever, with no
430
+ * terrain — the v1 flat single-elevation math stands in (one centroid
431
+ * sample × area, one side at a time), with the `stale` flag covering the
432
+ * in-flight window. Either way: `cutMeters3`/`fillMeters3`/`netMeters3`/
433
+ * `totalMeters3` are RAW geometric volumes — `deadband` zeroes out either
434
+ * side of a figure too small to be meaningful (drag jitter near h=0), but
435
+ * `swell`/`shrink` never touch them, so these four always answer "does the
436
+ * ground reach target elevation" regardless of whether a density/factor
437
+ * attribute happens to be configured. `cutAdjustedMeters3`/
438
+ * `fillAdjustedMeters3` are the SEPARATE material-adjusted figures (Bank/
439
+ * Loose/Compacted convention — see their own doc comments), derived from
440
+ * the raw (deadband-applied) cut/fill, not from area×height directly, so a
441
+ * zeroed-by-deadband side stays zero in its adjusted form too. Tonnage is
442
+ * likewise computed from the raw volume (mass-conserving — see
443
+ * `cutMassKg`'s doc comment).
444
+ */
445
+ private recomputeVolumeReadout;
446
+ /**
447
+ * Rebuilds the footprint + gizmo from current baseElevationM/heightOffsetM.
448
+ * Called on commit and on every gizmo-drag frame. At h=0 and while filling
449
+ * (h>0), the DRAPED layer and the extruded prism are mutually exclusive —
450
+ * one flat, one solid, never both populated. While CUTTING (h<0) the
451
+ * drape layer stays populated ALONGSIDE the extruded prism: the prism
452
+ * alone represents the excavation, but its own top face sits at the real
453
+ * terrain surface where it's unreliable to see (near-coplanar with the
454
+ * opaque terrain mesh) — the drape layer is what reliably keeps the
455
+ * original green footprint visible for a cut, matching how it already
456
+ * reads for fill/at-rest.
457
+ */
458
+ /** Clear the flat-target faces layer only when it actually holds any — see `flatFacesLive`. */
459
+ private clearFlatFaces;
460
+ private buildVolumeVisual;
461
+ /**
462
+ * Gizmo drag lifecycle — from om-map's forwarded RuntimeCore onGizmoDrag*
463
+ * callbacks (raw deck.gl PickingInfo, not a resolved Selection). Converts
464
+ * vertical screen-pixel delta to a world elevation delta by linearizing
465
+ * the viewport's projection once at grab-time (project the anchor at two
466
+ * known elevations 100m apart) — a local approximation valid for the
467
+ * gesture's duration since dragPan is suspended for the same duration, so
468
+ * the camera can't move underneath it.
469
+ */
470
+ handleGizmoDragStart(info: PickingInfo, event: GizmoDragEvent): void;
471
+ handleGizmoDrag(info: PickingInfo): void;
472
+ handleGizmoDragEnd(): void;
35
473
  private emptyReadout;
36
474
  private dispatch;
37
475
  private ensureLayers;
476
+ /**
477
+ * Volume mode's internal layers (spec: "Cut/fill volume measurement").
478
+ * TWO layers carry the footprint, only one populated at a time, mirroring
479
+ * a real-world reference implementation's own documented fix for the same
480
+ * problem (mutating `terrain`/extensions on a LIVE layer corrupts its
481
+ * shader pipeline — the fix is a distinct layer id per terrain mode, so
482
+ * deck tears down and rebuilds cleanly instead of patching in place):
483
+ * - VOLUME_DRAPE_ID: flat 2D ring, `terrain="drape"` — used exactly while
484
+ * `heightOffsetM === 0`, so the "ready" footprint sits seamlessly on
485
+ * the real terrain surface (same as the in-progress sketch preview),
486
+ * instead of floating above/sinking below it as a lone flat plane
487
+ * would on anything but dead-flat ground.
488
+ * - VOLUME_ID: extruded prism, `terrain="off"`, explicit `[lng,lat,z]` at
489
+ * each vertex's own sampled elevation (`vertexElevationsM` — not one
490
+ * repeated flat value, so the base ring hugs real ground undulation
491
+ * instead of visibly floating on sides away from the centroid sample)
492
+ * — used once the gizmo is dragged away from zero, when the shape
493
+ * needs a real, undistorted prism (a truly draped ring's per-vertex
494
+ * heights would twist the side walls).
495
+ * - VOLUME_CUT_GUIDES_ID: a LineLayer, one vertical strut per footprint
496
+ * vertex, populated only while cutting (heightOffsetM < 0). A cut's
497
+ * entire prism sits below the original ground, so it's fully occluded
498
+ * by the opaque terrain mesh from a normal view — `depthTest: false`
499
+ * (patched once below, not attribute-expressible) keeps these struts
500
+ * visible through it, the only way to SEE how deep a cut goes short of
501
+ * reading the numeric Cut readout.
502
+ * Plus the draggable height gizmo: two SimpleMeshLayers (GIZMO_SHAFT_ID a
503
+ * cylinder, GIZMO_CONE_ID a cone reused for both the top cap and the
504
+ * inverted bottom one) sharing the static unit meshes built once at module
505
+ * load. All start with empty data; `buildVolumeVisual` fills in whichever
506
+ * pair is live on commit/drag.
507
+ */
508
+ private ensureVolumeLayers;
509
+ /**
510
+ * Numbered vertex badges for the profile, in draw order. Only meaningful
511
+ * with `profile` on (nothing else references vertex order), and only once a
512
+ * ring exists — so it clears itself in every other state rather than
513
+ * lingering on a deleted footprint.
514
+ */
515
+ private buildProfileVertexMarkers;
38
516
  private removeLayers;
39
517
  }
40
518
  export declare function getMeasureController(mapEl: Element, core: RuntimeCore): MeasureController;