pub struct Mesh {
pub positions: Vec<f32>,
pub normals: Vec<f32>,
pub indices: Vec<u32>,
pub rtc_applied: bool,
pub origin: [f64; 3],
pub instance_meta: Option<InstanceMeta>,
pub local_bounds: Option<[f32; 6]>,
pub local_to_world: Option<[f64; 16]>,
}Expand description
Triangle mesh
Fields§
§positions: Vec<f32>Vertex positions (x, y, z)
normals: Vec<f32>Vertex normals (nx, ny, nz)
indices: Vec<u32>Triangle indices (i0, i1, i2)
rtc_applied: boolWhether RTC offset has already been subtracted from positions.
Set by FacetedBrepProcessor::process_with_rtc to prevent
transform_mesh from double-subtracting RTC.
origin: [f64; 3]Per-mesh local origin (f64), in the RTC/world frame. When non-zero,
positions are stored RELATIVE to this origin (so they stay small and
f32-precise regardless of the element’s world placement), and the world
position of a vertex is origin + position. Set by transform_mesh_world
to the element’s centroid so building-scale coordinates (~hundreds of
metres) never collapse adjacent vertices to bit-identical f32. Default
[0, 0, 0] means positions are already absolute (legacy/local meshes).
instance_meta: Option<InstanceMeta>Instancing side-channel (see InstanceMeta); None on the flat path.
local_bounds: Option<[f32; 6]>Local (pre-placement, object-space) AABB — positions bounds as they
were BEFORE apply_placement’s transform was baked in. None for an
empty mesh or one that never went through transform_mesh_world_framed
(e.g. synthetic/test meshes). Unrelated to origin, which is a
world-space translation captured AFTER the transform, purely for f32
precision — see issue #1474.
local_to_world: Option<[f64; 16]>The resolved IfcLocalPlacement chain applied to this mesh by
apply_placement (row-major, same convention as
InstanceMeta::transform). None when no placement was applied
(synthetic/test meshes) — see issue #1474.
Implementations§
Source§impl Mesh
impl Mesh
Sourcepub fn with_capacity(vertex_count: usize, index_count: usize) -> Self
pub fn with_capacity(vertex_count: usize, index_count: usize) -> Self
Create a mesh with capacity
Sourcepub fn rebuilt_like(
&self,
positions: Vec<f32>,
normals: Vec<f32>,
indices: Vec<u32>,
) -> Mesh
pub fn rebuilt_like( &self, positions: Vec<f32>, normals: Vec<f32>, indices: Vec<u32>, ) -> Mesh
Build a mesh with FRESH geometry buffers (positions / normals /
indices) that carries THIS mesh’s placement/frame metadata forward:
origin (RTC / local-frame translation), rtc_applied, local_bounds
and local_to_world (the #1474 placement capture).
This is the correct constructor for an in-place rebuild pass that
REPLACES the vertex buffers of an already-placed mesh (sliver refine,
subdivide, weld). Constructing a bare Mesh and copying back only a
field or two silently resets origin and the #1474 capture to their
defaults, which mis-places the rebuilt host at the world origin on
local-framed (large / georeferenced) models — see facet_weld’s
sliver-refine and this module’s subdivide_once / weld_impl.
instance_meta is intentionally NOT carried. Every such rebuild CHANGES
the vertices, so the mesh no longer reproduces its representation’s
canonical geometry; carrying the (vertex-invariant) rep_identity
forward would let the GPU-instancing collator dedup this changed mesh
against an unrefined sibling that shares the same rep_identity and
draw the wrong geometry. Dropping it mirrors the void-cut path, which
nulls instance_meta for exactly this reason.
§Precondition
The new buffers MUST NOT extend the mesh’s spatial extent beyond the
original: the carried local_bounds stays valid only because it remains
a superset of the rebuilt vertices’ extent. This holds for every
current caller — sliver-refine and subdivide insert edge/interior
midpoints (convex combinations that lie inside the existing hull), and
weld only merges/moves coincident vertices to a snapped position (a
subset extent). A future caller that GROWS the extent (adds vertices
outside the original hull) must NOT use this constructor for
local_bounds: it has to recompute local_bounds from the new positions
or pass through a variant that sets it to None.
Sourcepub fn from_triangle(
v0: &Point3<f64>,
v1: &Point3<f64>,
v2: &Point3<f64>,
normal: &Vector3<f64>,
) -> Self
pub fn from_triangle( v0: &Point3<f64>, v1: &Point3<f64>, v2: &Point3<f64>, normal: &Vector3<f64>, ) -> Self
Create a mesh from a single triangle
Sourcepub fn add_vertex(&mut self, position: Point3<f64>, normal: Vector3<f64>)
pub fn add_vertex(&mut self, position: Point3<f64>, normal: Vector3<f64>)
Add a vertex with normal
Sourcepub fn add_triangle(&mut self, i0: u32, i1: u32, i2: u32)
pub fn add_triangle(&mut self, i0: u32, i1: u32, i2: u32)
Add a triangle
Sourcepub fn merge(&mut self, other: &Mesh)
pub fn merge(&mut self, other: &Mesh)
Merge another mesh into this one.
Positions are stored relative to origin. The common case is merging
local/origin-zero meshes (sub-meshes combined BEFORE the world transform),
where origins match and concatenation is exact. If the two meshes carry
different non-zero origins, other is rebased into self’s frame so the
merged positions stay consistent (correct, though large-coordinate if the
origins are far apart — which the pre-transform merge order avoids).
Sourcepub fn merge_all(&mut self, meshes: &[Mesh])
pub fn merge_all(&mut self, meshes: &[Mesh])
Batch merge multiple meshes at once (more efficient than individual merges)
Sourcepub fn vertex_count(&self) -> usize
pub fn vertex_count(&self) -> usize
Get vertex count
Sourcepub fn triangle_count(&self) -> usize
pub fn triangle_count(&self) -> usize
Get triangle count
Sourcepub fn subdivided(&self, levels: usize) -> Mesh
pub fn subdivided(&self, levels: usize) -> Mesh
Uniform 1→4 midpoint subdivision applied levels times. Each triangle is
split into four by its three edge midpoints; midpoint positions/normals are
the f32 average of the edge endpoints (commutative ⇒ a shared edge yields
the SAME midpoint from either adjacent triangle, so the result stays
watertight once the kernel’s interner welds coincident vertices).
Purpose: a host face that is one or two huge triangles concentrates ALL of
a wall’s opening cuts onto it, so the exact arrangement re-triangulates a
single triangle carrying dozens of constraint segments — O(k²) and, worse,
dense enough that the batched N-ary subtract leaves unrecovered constraints
and falls back to the O(N²) sequential path. Spreading the face into many
small triangles localises each opening to a few of them (small k), so the
batched cut recovers. consolidate_coplanar re-triangulates each coplanar
group afterwards, so the extra interior vertices do not survive into the
final mesh except where a hole boundary pins them.
Sourcepub fn validate_indices(&mut self)
pub fn validate_indices(&mut self)
Remove triangle indices that reference vertices beyond the positions array. This prevents panics from malformed IFC data (e.g. Revit exports with invalid indices).
Sourcepub fn drop_degenerate_triangles(&mut self)
pub fn drop_degenerate_triangles(&mut self)
Drop triangles that collapsed into degenerate needles when the mesh was stored at f32 precision.
At building-scale world coordinates (e.g. ~220 m) an f32 mantissa only resolves ~15 µm, so two genuinely-distinct vertices less than one ULP apart round to the same (or near-same) f32 value. The triangle that joined them becomes a zero-area sliver — and when its third vertex is far away, a long thin “fan” that visibly spans the model (the gross corruption seen on large georeferenced buildings).
These slivers carry effectively no area, so the neighbouring triangles of the same face already cover the surface; removing them is visually lossless while eliminating the fans. The proper fix (local-frame / tiled vertex storage) keeps the vertices distinct in the first place; this is the backstop for meshes that still arrive degenerate.
Conservative by design — only drops triangles that are unambiguously
garbage: a bit-identical f32 vertex pair (exact zero area) or an aspect
ratio (longest edge / shortest edge) above 1e5. Legitimate thin members
(mullions, braces) sit far below that. Only indices change; the vertex
buffer and per-vertex data are left intact, so the operation is
deterministic and keeps vertex indices stable.
Sourcepub fn bounds(&self) -> (Point3<f32>, Point3<f32>)
pub fn bounds(&self) -> (Point3<f32>, Point3<f32>)
Calculate bounds (min, max) - optimized with chunk iteration
Sourcepub fn centroid_f64(&self) -> Point3<f64>
pub fn centroid_f64(&self) -> Point3<f64>
Calculate centroid in f64 precision (for RTC offset calculation) Returns the average of all vertex positions
Sourcepub fn welded_by_position(&self, position_eps: f32) -> Mesh
pub fn welded_by_position(&self, position_eps: f32) -> Mesh
Weld vertices that share a position, regardless of normal.
Returns a new mesh where vertices at the same position (within
position_eps) collapse to one canonical vertex; the welded
vertex’s normal is the sum of contributing normals, re-normalized
(or a neutral up-Z (0, 0, 1) default if the sum is degenerate,
e.g. exactly opposing normals cancelling out).
Triangles that collapse to a degenerate edge or point are dropped.
Use this when you need a topologically connected, manifold- candidate mesh — volume queries, CSG operands, watertight checks, mesh repair pipelines. Shading at sharp corners gets averaged.
position_eps is the bucket size in metres (1 µm is a safe
default for IFC).
Sourcepub fn drop_thin_triangles(&mut self, h_eps: f64)
pub fn drop_thin_triangles(&mut self, h_eps: f64)
Drop triangles whose perpendicular height (= 2·area / longest edge) is
below h_eps metres — i.e. genuinely-degenerate collinear slivers
(three distinct but near-collinear vertices, zero area). These come from
redundant collinear vertices in source brep faces / extrusion profiles
triangulated as-is; vertex welding can’t merge them (the vertices are
distinct), so this catches them. At h_eps ≈ 15 µm — far below any real
architectural feature — the dropped triangles carry no area, so the
surrounding triangulation still covers the face (visually lossless,
watertight-preserving). Only indices change.
Sourcepub fn clean_degenerate(&mut self)
pub fn clean_degenerate(&mut self)
Mesh hygiene applied to every element mesh before it leaves the router.
Restores the cleanup the pure-Rust pipeline lost when #1024 removed Manifold (which implicitly dropped degenerate output). Without it, redundant/near-collinear source vertices in faceted breps and extrusion profiles get triangulated into visible needle “spikes” and jagged silhouettes (the regression reported on large breps); BIMcollab and other viewers don’t show them because they clean degenerates on import.
Deliberately does not weld vertices. The pipeline emits per-face
flat-shaded facet soup on purpose (each facet keeps its own vertices +
normal so creases stay sharp — see issue #846); welding would share
vertices across facets and re-smooth every crease. Instead we drop only
the genuinely-degenerate triangles via
drop_thin_triangles below the kernel’s
reconcile grid (1/65536 ≈ 15.3 µm): coincident-pair needles (area 0)
and collinear slivers (three distinct near-collinear vertices). The grid
is the kernel’s own representable resolution, so sub-grid triangles are
degenerate by definition; measured triangle counts are flat from
10–50 µm and only start touching real geometry at ~100 µm (6.5× higher),
confirming nothing real lives in that band. Positions/normals are left
untouched, so it is visually lossless and bit-deterministic.
The 15.3 µm threshold is most precise when applied in a small-magnitude
(element-local) frame, where f32 positions resolve well below it — which
the tessellation chokepoints honour (they clean before world
placement). The void-cut output is cleaned in world coordinates (the cut
runs there), so on a model georeferenced a few hundred metres to ~10 km
from origin — below the RTC re-basing threshold — the f32 grid at that
magnitude approaches the threshold and the margin near opening seams
erodes slightly; the longest <= 0 guard still catches full collapse at
extreme scale. NaN/Inf triangles are kept (the comparison is false),
i.e. non-finite geometry is left for upstream to handle, never dropped.
Sourcepub fn clip_triangles_to_aabb(
&mut self,
min: [f32; 3],
max: [f32; 3],
pad: f32,
) -> usize
pub fn clip_triangles_to_aabb( &mut self, min: [f32; 3], max: [f32; 3], pad: f32, ) -> usize
Drop triangles with ANY vertex outside [min - pad, max + pad], then
compact away the now-unreferenced vertices. Returns the count dropped.
Boolean subtraction can only REMOVE material, so the cut of a host whose
pre-cut AABB is [min, max] is mathematically contained in that AABB.
A malformed cutter — self-intersecting, or carrying garbage vertices
metres from the real opening (e.g. an exporter that welds stray points
into a tessellated void, the multi-body-cutter case) — can make the
exact mesh-arrangement leak a spurious far-flung “flap” triangle into the
output: a visible spike poking metres out of the wall. Such a triangle
only appears once a SECOND cutter perturbs the arrangement, so it slips
past the per-cutter admission guards. Any output vertex beyond the host
AABB (past pad, which absorbs kernel snap / f64→f32 round-trip jitter)
is provably such an artifact, so the triangle is dropped and its orphaned
vertices removed (they would otherwise skew bounds() and every
AABB-derived consumer: framing, picking, clash, export).
A no-op on clean cuts — when nothing lies outside, positions/normals
are left bit-identical so the frozen snapshot corpus is unperturbed. Also
a no-op in the degenerate case where EVERY triangle would be dropped (an
upstream frame/placement bug, not a cut artifact): the mesh is preserved
rather than silently emptied.
Sourcepub fn clip_triangles_to_host_aabb(
&mut self,
min: [f32; 3],
max: [f32; 3],
) -> usize
pub fn clip_triangles_to_host_aabb( &mut self, min: [f32; 3], max: [f32; 3], ) -> usize
Clip a void-cut result to the host’s pre-cut AABB [min, max], dropping
any triangle poking beyond it (see Mesh::clip_triangles_to_aabb). A
subtract can only remove material, so anything past the host AABB is a cut
artifact. The tolerance absorbs f64→f32 round-trip jitter (sub-mm), so it
is a small ABSOLUTE band, NOT a fraction of host size: an unbounded
1e-3 * diag reaches 0.13 m on a 130 m floor slab — wider than the
~0.105 m flush-cap reveal overhang it must trap, which is why only large
slabs/roofs leaked it (a 5 m wall’s 5 mm pad already trims the identical
overhang, #1633). Clamped to [5 mm, 10 mm]: byte-identical to the former
1e-3 * diag for hosts ≤ 10 m diagonal (1e-3 * diag ≤ 1e-2), trimming
on every larger one. Returns the count dropped.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Mesh
impl RefUnwindSafe for Mesh
impl Send for Mesh
impl Sync for Mesh
impl Unpin for Mesh
impl UnsafeUnpin for Mesh
impl UnwindSafe for Mesh
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.