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