1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Per-entity geometry fingerprinting for model diffing.
//!
//! The viewer's "compare two revisions" feature needs a stable per-entity
//! signature so an unchanged element hashes identically across two files,
//! while a genuine edit (moved, or reshaped so the surface itself changes)
//! hashes differently. Re-cutting an unchanged surface over the SAME corners is
//! *not* an edit and deliberately does not move the hash — see
//! **Retriangulation-invariant** below for the exact scope of that guarantee,
//! and [`GeometryHasher::finish`] for what the fingerprint can and cannot
//! distinguish.
//!
//! ## Design invariants
//!
//! * **RTC-invariant.** Each file independently shifts world coordinates toward
//! the origin (Relative-To-Center) to preserve `f32` precision. That shift is
//! a property of the *file*, not the element, and the base and head files may
//! pick different offsets. We therefore hash in reconstructed **world**
//! coordinates (`local + rtc_offset`), so the same wall in the same world
//! spot hashes the same regardless of each file's RTC choice.
//! * **Translation-sensitive.** Because we hash absolute world position, an
//! element that genuinely *moved* hashes differently — a moved element is an
//! edit ("orange"), not "unchanged".
//! * **Order/winding-invariant.** Triangle order, vertex-buffer order, and
//! winding are implementation details of the geometry kernel, not the shape.
//! Each triangle's three quantized vertices are sorted before hashing, and
//! triangles are combined commutatively, so reordering/rewinding does not move
//! the hash.
//! * **Retriangulation-invariant, over a fixed vertex set.** So is the
//! triangulator's DIAGONAL CHOICE. The hash is therefore taken over the
//! SURFACE, in two channels re-cutting cannot move — the SET of distinct
//! quantized vertices, and the total area within each supporting PLANE (see
//! [`surface`]).
//!
//! The guarantee is exactly this: re-cutting a region over the corners it
//! already has (a re-split diagonal, a re-rooted fan) does not move the hash.
//! It does **not** extend to a tessellation that INTRODUCES vertices — a quad
//! refanned through a new centre point, or an edge split at a new midpoint,
//! adds a member to the vertex-set channel and so does hash differently, even
//! though the surface and its per-plane area are unchanged. Distinguishing
//! that from a genuine edit needs a channel this fingerprint does not have.
//! See [`GeometryHasher::finish`] for the rest of the limits.
//! * **Tolerance-quantized.** Positions are snapped to a grid of `tolerance`
//! metres before hashing. Larger tolerance absorbs float noise (fewer false
//! "changed") at the cost of missing sub-tolerance edits. See
//! [`DEFAULT_GEOM_HASH_TOLERANCE`] and the `tolerance_sweep` test for the
//! trade-off — the effective floor is the `f32` precision of the local
//! positions (~1e-4 m near origin), so tolerances below ~1 mm mostly hash
//! float noise. A request finer than [`MIN_GEOM_HASH_TOLERANCE`] is clamped
//! up to it: below that grid, [`surface::plane_of`]'s `i128` plane-offset
//! arithmetic is an overflow surface on a georeferenced model, not a
//! precision win — see that constant for the measured bound.
//!
//! All inputs must be in a single consistent frame for both files (i.e. unit
//! scaled to metres, and either both pre- or both post- any axis convention
//! swap). The caller is responsible for feeding `positions` and `rtc_offset`
//! in the same frame.
//!
//! ## World AABB (#1891 follow-on)
//!
//! The same pass also accumulates an UNQUANTIZED `f64` world axis-aligned
//! bounding box ([`GeometryHasher::world_aabb`]). The hash alone cannot say
//! WHY two revisions differ — "hash changed" conflates moved, reshaped and
//! re-tessellated — so the diff engine needs a second, interpretable signal.
//! The box is free here: `add_mesh_with_origin` already reconstructs the exact
//! `f64` world coordinate of every triangle corner in order to quantize it.
//!
//! ## Volume, and its gate (#1891)
//!
//! [`GeometryHasher::volume`] is the divergence-theorem volume of the same
//! geometry — but only for entities whose produced mesh is PROVABLY a single
//! closed orientable solid. That proof comes from
//! [`crate::orient_mesh_outward_verdict`], which the producer runs on each
//! segment immediately before feeding it here; the hasher cannot derive it
//! itself, because the adjacency needed to decide closedness is exactly what
//! that pass builds.
//!
//! Everything else gets `None`. Read [`GeometryHasher::volume`] before
//! loosening any clause of that gate — each one is there because a specific,
//! measured class of element reports a confidently wrong number without it,
//! and none of the wrong numbers look wrong. [`GeometryClosure`] rides along so
//! a consumer can say WHICH clause refused.
/// The volume gate (`GeometryClosure` + `GeometryHasher::volume`). A CHILD
/// module, not a sibling, so it can read this module's private accumulators
/// without widening their visibility.
pub use GeometryClosure;
/// The world AABB (`GeometryHasher::extend_bounds` + `::world_aabb`). A CHILD
/// module, not a sibling, so it can read this module's private accumulators
/// without widening their visibility.
/// The two surface channels the fingerprint is built from. A CHILD module, not
/// a sibling, so it can use this module's private `mix64`/`fold_i64`.
/// The per-segment triangle fold (`add_mesh` / `add_mesh_with_origin` /
/// `add_oriented_mesh`). A CHILD module, not a sibling, so it can read this
/// module's private accumulators without widening their visibility.
/// Default quantization grid in metres (1 mm). Chosen as a starting point near
/// the `f32` precision floor of RTC-local coordinates; `tolerance_sweep` only
/// exercises a synthetic cube today; real-revision-pair calibration is still
/// open (see that test's doc comment).
///
/// Safety margin is narrower than the "near-origin" framing above suggests:
/// measured `f32` ULP is 9.77e-4 m (98% of this bucket) at both 8192 m and
/// 10 km, only just under 1 mm before crossing it at 16384 m.
///
/// What it actually depends on is the **largest absolute post-rebase
/// coordinate** staying under ~16384 m (where the `f32` ULP crosses 1 mm) — an
/// incidental dependency, not a designed one. Note that is a magnitude, not a
/// span: a model centred on the origin can span ~32 km and still satisfy it,
/// while one sitting 20 km out fails it however small it is. RTC re-centring
/// makes the condition typical but does not guarantee it, in two ways worth
/// stating rather than implying:
///
/// - `rtc_offset_from_translations` takes the **median** element translation
/// and returns `(0,0,0)` unless it exceeds
/// [`LARGE_COORD_THRESHOLD_METERS`](crate::LARGE_COORD_THRESHOLD_METERS).
/// A model whose bulk sits near the origin but whose outlying elements sit
/// on a national grid is therefore not re-centred at all, and those vertices
/// are hashed from `f32` world coordinates already past this bucket.
/// - Even when the rebase does fire, the offset it subtracts is the median
/// element translation, not the model's centre. A model is therefore not
/// centred on it, so an outlier can still land past 16384 m even when the
/// overall span would have fitted had the rebase been centred.
///
/// So raising that threshold is not the only thing that would need this
/// tolerance revisited; a far-flung or widely spread model reaches the same
/// place without any constant changing.
pub const DEFAULT_GEOM_HASH_TOLERANCE: f64 = 1.0e-3;
/// Floor on the quantization tolerance ([`GeometryHasher::new`] clamps any
/// smaller request up to this).
///
/// `plane_of`'s plane offset `d = n·point` is an `i128` product of a quantized
/// normal and a quantized corner, both scaled by `1/tolerance`; it grows
/// roughly as `1/tolerance²`. Measured on a georeferenced point (~2.6e6 m) and
/// a 100 m triangle, `tolerance = 1e-9` pushes `d` to ~1.6e38 — within a factor
/// of ~1 of `i128::MAX` (1.7e38), i.e. one differently-shaped input away from
/// overflow (debug builds panic, release wraps and two unrelated planes can
/// alias to the same key). At this floor the same inputs land `d` around
/// 1.6e26 — six orders of magnitude of headroom. It is also three orders of
/// magnitude finer than the documented useful floor (~1 mm, the `f32`
/// precision limit of RTC-local coordinates — see the module docs'
/// "Tolerance-quantized" bullet), so no real caller loses precision by being
/// clamped to it.
pub const MIN_GEOM_HASH_TOLERANCE: f64 = 1.0e-6;
/// splitmix64 finalizer — strong avalanche for a single `u64`. Shared with
/// `router::content_hash`'s 128-bit content hash, which uses this SAME
/// finalizer per lane.
pub
/// Fold one signed integer into a running hash (order-dependent).
/// Snap a world coordinate to the quantization grid.
///
/// `inv_tol` is `1.0 / tolerance`, hoisted out of the per-vertex loop.
/// Accumulates a single entity's geometry signature across one or more mesh
/// segments. Segments are combined commutatively, so the order in which the
/// kernel emits an entity's pieces does not affect the result.
/// Convenience: hash a single-segment entity in one call.