Skip to main content

GeometryHasher

Struct GeometryHasher 

Source
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

Source

pub fn closure(&self) -> GeometryClosure

The entity’s folded per-segment topology. See GeometryClosure.

Source

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.

Source

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

Source

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

Source

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.

Source

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.

Source

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

Source

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 to MIN_GEOM_HASH_TOLERANCE — see that constant for why a smaller request is an i128 overflow 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.
Source

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.

Source

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

Source§

fn clone(&self) -> GeometryHasher

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for GeometryHasher

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<U> As for U

Source§

fn as_<T>(self) -> T
where T: CastFrom<U>, U: Sized,

Casts self to type T. The semantics of numeric casting with the as operator are followed, so <T as As>::as_::<U> can be used in the same way as T as U for numeric conversions. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.