pub struct GeometryHasher { /* private fields */ }Expand description
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.
Implementations§
Source§impl GeometryHasher
impl GeometryHasher
Sourcepub fn closure(&self) -> GeometryClosure
pub fn closure(&self) -> GeometryClosure
The entity’s folded per-segment topology. See GeometryClosure.
Sourcepub fn retract_closure_if_mesh_edited(&mut self, triangles_dropped: u64)
pub fn retract_closure_if_mesh_edited(&mut self, triangles_dropped: u64)
Withdraw the topology verdict — and with it the volume — when the meshes
were EDITED after this hasher saw them. No-op for 0.
The producer takes each segment’s verdict where the orienter runs, which
is necessarily BEFORE the per-MeshData funnel that finishes the mesh.
One step in that funnel removes triangles: the f32-collapse degenerate
backstop (Mesh::drop_degenerate_triangles). A removed triangle takes
its three welded edges with it, so each neighbour along them drops from
two incidences to one — a BOUNDARY edge. A shell certified closed can
therefore be handed back OPEN while still carrying 0x0F and a finite
volume, which is exactly the “confidently wrong number” this gate exists
to refuse (Greptile review, PR #1993).
So the producer passes its per-element drop tally here before reading
Self::closure / Self::volume, and any drop at all retracts.
Why retract rather than RE-DERIVE the verdict on the cleaned mesh (which
a throwaway re-run of the orienter would give, exactly, since closedness
is winding-independent): the verdict is only half of it. volume6 was
accumulated over the PRE-cleanup triangle set, and a dropped needle’s
tetrahedron is not zero — its contribution scales with the lever arm to
the reference corner, metre-scale on a metre-scale body. A re-derived
verdict would license a stale number. Re-accumulating both would mean
re-running the orienter and the whole hash pass on every affected
element; refusing is sound, costs nothing, and moves no vertex.
The funnel’s OTHER post-verdict edit, mesh_weld::weld_indexed, needs no
such treatment: it merges only vertices with bit-identical f32
positions, a strict refinement of the orienter’s 10 µm weld grid, so the
welded edge graph the verdict was read off is unchanged.
Sourcepub fn volume(&self) -> Option<f64>
pub fn volume(&self) -> Option<f64>
The entity’s enclosed volume in cubic metres — Some ONLY when the
geometry that produced it is provably a single closed orientable solid,
None otherwise. THE RULE IS DELIBERATELY NARROW. Each clause of
GeometryClosure::is_trustworthy_solid rejects a specific way the
number would otherwise be silently wrong.
It is narrow, not useless: measured over the ara3d fixture corpus (33,701 elements that produced geometry, across 68 files) the gate admits 24,073 of them — 71.4% — and not one of those reports a volume exceeding its own bounding box.
§all_closed
Over an open surface the divergence sum is not approximate, it is
arbitrary: the boundary-loop flux scales with the distance to the
reference point, so the “volume” of a sheet is whatever you referenced
it to. Material-layer wall slices are open bands by construction since
#1311 (router/layers.rs refuses to cap them, because capping made every
shared interface a doubled coincident sheet), and IfcTriangulatedFaceSet
TINs / SurfaceModels are open by definition. Measured over the ara3d
fixture corpus at exactly this granularity — 73,626 segments across
33,701 elements — 16.2% of segments are not a single closed orientable
component, and 15.3% of elements have at least one such segment.
§all_orientable
A non-orientable component has no consistent inside, so the sign of each
triangle’s contribution is arbitrary. orient_mesh_outward already
refuses to re-wind these for the same reason.
§all_single_component
orient_mesh_outward flips each CLOSED component so its own signed
volume is POSITIVE. For two disjoint solids that is right. For a solid
whose cavity is a second, inner shell it is catastrophic: the sum reports
outer + cavity where the truth is outer − cavity. Distinguishing the
two needs a containment test, and the orientation already applied has
destroyed the sign that encoded the difference.
§segments == 1 — the multi-segment decision
A segment is one sub-mesh, which is one representation ITEM (or one
material-layer band). IFC treats an item list as an implicit UNION, and
exporters routinely emit items that overlap: a window frame and its sash
meeting at the rebate, lapped steel plates, a Clearance representation
whose RepresentationType is SweptSolid (which passes the body filter
in router/rep_filter.rs — nothing filters on the representation
IDENTIFIER), or two Body representations in different subcontexts.
A sum over overlapping items double-counts the intersection, and each
item on its own is a perfectly ordinary closed solid, so nothing about
the individual verdicts reveals it.
Measured, this is not a corner case. Of the 4,472 all-closed multi-segment elements in the corpus, 2,971 (66%) have a pair of segments whose world boxes overlap by more than 1% of the smaller box, and the most common failure is total containment (overlap fraction 1.000 — one item’s box entirely inside another’s), on doors, windows and furnishing assemblies. Summing them produces a volume larger than the element’s OWN bounding box — a geometric impossibility — on 987 of them (22%), with a p90 of 2.99× and a maximum of 3.00× the box. Restricting to a single segment leaves 0 such elements, with a p50 fill of 0.96 and a maximum of exactly 1.00.
AABB-disjointness was considered as a weaker gate and rejected: it is unsound in both directions (two interlocking L-members have overlapping boxes and disjoint solids; two overlapping solids can share one box), and a real disjointness test is a CSG intersection per pair.
There is also a consistency argument that needs no measurement. When the
sub-mesh path fails, produce_element_meshes falls back to
process_element, which merges every item into ONE mesh. That merged
mesh has two components, so all_single_component already rejects it.
Accepting the sum on the sub-mesh path would mean the same element
reports a volume or not depending on which router entry point happened to
succeed. segments == 1 makes the two paths agree.
§What this canNOT certify
Closedness is a property of the SURFACE, not evidence that the surface is
the RIGHT one. When the #1109 CSG budget trips, apply_void_context
returns the UNCUT host (router/voids/mod.rs), which is still a flawless
closed solid — it just still contains its openings. That over-reports and
this verdict cannot see it. A consumer that cares must also read
ProducedElementMeshes::csg_failures, which is where that degradation is
reported.
Source§impl GeometryHasher
impl GeometryHasher
Sourcepub fn world_aabb(&self) -> Option<[f64; 6]>
pub fn world_aabb(&self) -> Option<[f64; 6]>
The entity’s world-space AABB as [minx, miny, minz, maxx, maxy, maxz],
or None if the box is not well-formed on all three axes — which
covers both “no in-range triangle corner was ever seen” and the
partial-accumulation case below.
§Why all three axes are tested, not just one
The axes look like they must accumulate together — extend_bounds runs
the same loop over all three for every corner — but they do not, because
that loop is f64::min/f64::max, which DROP a NaN operand. A position
buffer carrying NaN on one axis and finite values on the others leaves
that axis at its INFINITY..NEG_INFINITY sentinel while its neighbours
hold real bounds. Testing only axis 0 then returns
Some([x0, inf, z0, x1, -inf, z1]) — an inverted, infinite axis
presented as a measured box, which downstream differences to NaN.
Requiring every axis to be finite and ordered turns that into None,
which the wire format already reserves NaN slots for
(MeshCollection::push_geometry_hash).
The hash makes no such promise: NaN quantizes to 0, so a NaN-carrying
entity still produces a fingerprint. Some(hash) with None box is
therefore a REACHABLE pair, not a structural impossibility, and
produce_element_meshes keeps the hash when it happens rather than
discarding both. The fingerprint is the diff engine’s primary signal and
is well-defined here; dropping it would remove the element from the
comparison in exchange for nothing, and it cannot desynchronize the
parallel FFI arrays because push_geometry_hash writes six NaNs for a
missing box instead of shortening the array (pinned by
a_missing_box_reserves_its_slots_instead_of_shifting_the_array).
The converse stays impossible: a Some box needs an accumulated corner,
which needs a triangle, which is_empty() already gates on.
UNQUANTIZED f64 world coordinates, not grid indices: the box is meant
to be read as a length, so snapping it to the hash tolerance would put a
millimetre of noise on every face for no benefit. It is RTC-invariant
for the same reason the hash is — both are built from the reconstructed
world coordinate.
This is the diff engine’s “did it MOVE?” signal. The hash answers only “is it different”; comparing two boxes separates a translation (same extent, shifted centre) from a reshape (different extent) from pure re-tessellation (identical box, different hash).
The companion Self::volume exists but is far narrower — it is
None for a large minority of entities, by design.
Source§impl GeometryHasher
impl GeometryHasher
Sourcepub fn add_mesh(&mut self, positions: &[f32], indices: &[u32])
pub fn add_mesh(&mut self, positions: &[f32], indices: &[u32])
Add one mesh segment (a flat [x,y,z, ...] position buffer and a
triangle index buffer). Indices that run past the position buffer or
trailing non-triangle remainder are skipped defensively.
Sourcepub fn add_mesh_with_origin(
&mut self,
positions: &[f32],
indices: &[u32],
origin: [f64; 3],
)
pub fn add_mesh_with_origin( &mut self, positions: &[f32], indices: &[u32], origin: [f64; 3], )
Like Self::add_mesh but for positions stored in a per-element LOCAL
frame: origin (the per-mesh AABB-centre origin) is folded back so the
hash is over absolute world coordinates. This keeps the fingerprint
identical whether the producer emitted absolute positions (native) or
local + origin (the wasm local-frame path), and still detects element
MOVES.
The segment carries no topology verdict, so it counts as NOT a closed
solid and permanently disarms Self::volume. Producers that ran
crate::orient_mesh_outward_verdict on this exact buffer should call
Self::add_oriented_mesh instead.
Sourcepub fn add_oriented_mesh(
&mut self,
positions: &[f32],
indices: &[u32],
origin: [f64; 3],
verdict: OrientVerdict,
)
pub fn add_oriented_mesh( &mut self, positions: &[f32], indices: &[u32], origin: [f64; 3], verdict: OrientVerdict, )
Self::add_mesh_with_origin for a segment the producer just ran the
outward-orienter over, passing that pass’s OrientVerdict along.
verdict MUST describe this exact position/index buffer — the volume
below is only as honest as the closedness claim behind it. Anything
short of a single closed orientable component disarms the element’s
volume permanently; see Self::volume.
Source§impl GeometryHasher
impl GeometryHasher
Sourcepub fn new(tolerance: f64, rtc_offset: [f64; 3]) -> Self
pub fn new(tolerance: f64, rtc_offset: [f64; 3]) -> Self
Create a hasher for one entity.
tolerance— quantization grid in metres (must be> 0). Clamped up toMIN_GEOM_HASH_TOLERANCE— see that constant for why a smaller request is ani128overflow surface in [surface::plane_of], not a precision win.rtc_offset— the file’s RTC offset, added back to local positions to reconstruct world coordinates. Pass[0.0; 3]if positions are already in world space.
Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
true until at least one (non-degenerate, in-range) triangle has been
hashed. Lets callers skip emitting a fingerprint for entities that
produced no geometry.
Sourcepub fn finish(&self) -> u64
pub fn finish(&self) -> u64
Finalize the entity’s geometry hash: the distinct-vertex sum and the per-plane area total.
§What a difference here means, and what it does not
Two entities hash the same when they use the same set of quantized world vertices AND every plane carries the same total area. That covers the invariances the surface actually has — retriangulation, a re-rooted fan, triangle/segment order, winding — and still separates every genuine edit measured against it: a move, a scale, a face lifted out of its plane (new plane key), and faces deleted, whether or not their corners survive elsewhere in the mesh (the area falls either way).
It is deliberately a weaker discriminator than the triangle set it replaced. What it can no longer separate: two arrangements over the SAME vertex set giving every plane the same total area (retriangulation is the benign member of that family; a re-cut into a different region of equal area on the same corners is the malign one, and is not something a re-export produces), and a change of TRIANGLE COUNT alone — the count is no longer folded in, being exactly what a retriangulation changes.
Unchanged from before: winding is invisible, as is anything below the quantization grid.
Trait Implementations§
Source§impl Clone for GeometryHasher
impl Clone for GeometryHasher
Source§fn clone(&self) -> GeometryHasher
fn clone(&self) -> GeometryHasher
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl Freeze for GeometryHasher
impl RefUnwindSafe for GeometryHasher
impl Send for GeometryHasher
impl Sync for GeometryHasher
impl Unpin for GeometryHasher
impl UnsafeUnpin for GeometryHasher
impl UnwindSafe for GeometryHasher
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.