use super::*;
impl BrepSolid {
pub fn validate(&self) -> Vec<ValidationIssue> {
self.validate_with_tolerances(&KernelTolerances::for_solid(self, 1e-7))
}
pub(crate) fn validate_memoised(&self) -> Vec<ValidationIssue> {
if !validate_memo_on() {
return self.validate();
}
let key_timing = crate::blend::BlendTimer::start("timing.validate.memo_key_ms");
let Some(key) = self.validate_memo_key() else {
return self.validate();
};
key_timing.stop();
if VALIDATED_CLEAN.with(|clean| clean.borrow().contains(&key)) {
crate::blend::record_count("count.validate.memo_hit", 1);
return Vec::new();
}
crate::blend::record_count("count.validate.memo_miss", 1);
let issues = self.validate();
if issues.is_empty() {
VALIDATED_CLEAN.with(|clean| {
let mut clean = clean.borrow_mut();
if clean.len() >= VALIDATED_CLEAN_CAP {
clean.clear();
}
clean.insert(key);
});
}
issues
}
pub fn validate_with_tolerances(&self, tolerances: &KernelTolerances) -> Vec<ValidationIssue> {
self.validate_detailed(tolerances).issues
}
pub fn validate_detailed(&self, tolerances: &KernelTolerances) -> ValidationReport {
self.validate_detailed_scoped(tolerances, None)
}
pub fn validate_scoped(&self, geometry_faces: &HashSet<u64>) -> Vec<ValidationIssue> {
self.validate_detailed_scoped(&KernelTolerances::for_solid(self, 1e-7), Some(geometry_faces))
.issues
}
pub fn validate_detailed_scoped(
&self,
tolerances: &KernelTolerances,
geometry_faces: Option<&HashSet<u64>>,
) -> ValidationReport {
let mut issues = Vec::new();
let mut wire_warnings = Vec::new();
let mut max_pcurve_error = 0.0f64;
let mut bbox_lo = crate::Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut bbox_hi = crate::Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for vertex in &self.vertices {
bbox_lo.x = bbox_lo.x.min(vertex.point.x);
bbox_lo.y = bbox_lo.y.min(vertex.point.y);
bbox_lo.z = bbox_lo.z.min(vertex.point.z);
bbox_hi.x = bbox_hi.x.max(vertex.point.x);
bbox_hi.y = bbox_hi.y.max(vertex.point.y);
bbox_hi.z = bbox_hi.z.max(vertex.point.z);
}
let model_diagonal = if self.vertices.is_empty() {
0.0
} else {
bbox_hi.sub(bbox_lo).length()
};
let pcurve_limit = tolerances.pcurve_acceptance(model_diagonal);
let vertex_match = tolerances
.heal_band(model_diagonal, 2e-5)
.max(crate::VERTEX_MATCH_FLOOR);
let vertices: HashMap<u64, &VertexRecord> = self
.vertices
.iter()
.map(|vertex| (vertex.id, vertex))
.collect();
let edges: HashMap<u64, &EdgeRecord> =
self.edges.iter().map(|edge| (edge.id, edge)).collect();
if vertices.len() != self.vertices.len() {
issues.push(ValidationIssue::error("duplicate vertex id"));
}
if edges.len() != self.edges.len() {
issues.push(ValidationIssue::error("duplicate edge id"));
}
let mut edge_uses: HashMap<u64, Vec<bool>> = HashMap::default();
let mut face_ids = HashSet::default();
for shell in &self.shells {
for face in &shell.faces {
if !face_ids.insert(face.id) {
issues.push(ValidationIssue::error(format!(
"duplicate face id {}",
face.id
)));
}
let read_geometry =
geometry_faces.is_none_or(|scope| scope.contains(&face.id));
let surface = if read_geometry {
match NurbsSurface::new(
face.surface.degree_u,
face.surface.degree_v,
face.surface.knots_u.clone(),
face.surface.knots_v.clone(),
face.surface.control_points.clone(),
) {
Ok(surface) => Some(surface),
Err(error) => {
issues.push(ValidationIssue::error(format!(
"face {} has invalid surface: {}",
face.id, error
)));
continue;
}
}
} else {
None
};
for (loop_index, loop_record) in face.loops.iter().enumerate() {
if loop_record.coedges.is_empty() {
issues.push(ValidationIssue::error(format!(
"loop {} of face {} is empty",
loop_record.id, face.id
)));
continue;
}
for coedge in &loop_record.coedges {
edge_uses
.entry(coedge.edge_id)
.or_default()
.push(coedge.forward);
}
for index in 0..loop_record.coedges.len() {
let current = &loop_record.coedges[index];
let next = &loop_record.coedges[(index + 1) % loop_record.coedges.len()];
let Some(current_edge) = edges.get(¤t.edge_id) else {
issues.push(ValidationIssue::error(format!(
"coedge {} references missing edge {}",
current.id, current.edge_id
)));
continue;
};
let Some(next_edge) = edges.get(&next.edge_id) else {
issues.push(ValidationIssue::error(format!(
"coedge {} references missing edge {}",
next.id, next.edge_id
)));
continue;
};
let current_end = if current.forward {
current_edge.end_vertex_id
} else {
current_edge.start_vertex_id
};
let next_start = if next.forward {
next_edge.start_vertex_id
} else {
next_edge.end_vertex_id
};
if current_end != next_start {
issues.push(ValidationIssue::error(format!(
"loop {} of face {} is open between coedges {} and {}",
loop_record.id, face.id, current.id, next.id
)));
}
let Some(surface) = &surface else {
continue;
};
let pcurve = match NurbsCurve::new(
current.pcurve.degree,
current.pcurve.knots.clone(),
current.pcurve.control_points.clone(),
) {
Ok(curve) => curve,
Err(error) => {
issues.push(ValidationIssue::error(format!(
"coedge {} has invalid pcurve: {}",
current.id, error
)));
continue;
}
};
let curve = match NurbsCurve::new(
current_edge.curve.degree,
current_edge.curve.knots.clone(),
current_edge.curve.control_points.clone(),
) {
Ok(curve) => curve,
Err(error) => {
issues.push(ValidationIssue::error(format!(
"edge {} has invalid curve: {}",
current_edge.id, error
)));
continue;
}
};
match memoized_coedge_error(
surface,
&pcurve,
&curve,
current_edge,
current.forward,
pcurve_limit,
) {
Ok(error) => {
max_pcurve_error = max_pcurve_error.max(error);
if error > pcurve_limit {
issues.push(ValidationIssue::error(format!(
"coedge {} of face {} pcurve is inconsistent with edge {} \
(max deviation {:.6}, limit {:.6})",
current.id, face.id, current_edge.id, error, pcurve_limit,
)));
}
}
Err(error) => issues.push(ValidationIssue::error(format!(
"coedge {} of face {} cannot be evaluated: {}",
current.id, face.id, error
))),
}
}
let Some(surface) = &surface else {
continue;
};
if let Some(warning) = validate_uv_wire(
surface,
loop_record,
&edges,
tolerances,
loop_index == 0,
face.same_sense,
) {
wire_warnings.push(ValidationIssue::warning(format!(
"face {} loop {}: {}",
face.id, loop_record.id, warning
)));
}
}
if let Some(surface) = &surface {
if let Some(error) = validate_periodic_boundary_winding(surface, face, tolerances) {
issues.push(ValidationIssue::error(format!("face {}: {error}", face.id)));
}
}
}
}
for edge in &self.edges {
if !(edge.t0.is_finite() && edge.t1.is_finite() && edge.t0 < edge.t1) {
issues.push(ValidationIssue::error(format!(
"edge {} has invalid parameter range",
edge.id
)));
}
let Some(start) = vertices.get(&edge.start_vertex_id) else {
issues.push(ValidationIssue::error(format!(
"edge {} references missing start vertex {}",
edge.id, edge.start_vertex_id
)));
continue;
};
let Some(end) = vertices.get(&edge.end_vertex_id) else {
issues.push(ValidationIssue::error(format!(
"edge {} references missing end vertex {}",
edge.id, edge.end_vertex_id
)));
continue;
};
if let Ok(curve) = NurbsCurve::new(
edge.curve.degree,
edge.curve.knots.clone(),
edge.curve.control_points.clone(),
) {
if let Ok(point) = curve.evaluate(edge.t0) {
let gap = point.sub(start.point).length();
if gap > vertex_match {
issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: true }, format!(
"edge {} curve start does not match vertex {} \
(gap={gap:.9}, curve={point:?}, vertex={:?})",
edge.id, start.id, start.point
)));
}
}
if let Ok(point) = curve.evaluate(edge.t1) {
let gap = point.sub(end.point).length();
if gap > vertex_match {
issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: false }, format!(
"edge {} curve end does not match vertex {} \
(gap={gap:.9}, curve={point:?}, vertex={:?})",
edge.id, end.id, end.point
)));
}
}
}
let uses = edge_uses.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
if edge.degenerate {
let valid = uses.len() == 1 || (uses.len() == 2 && uses[0] != uses[1]);
if !valid {
issues.push(ValidationIssue::error(format!(
"degenerate edge {} has invalid incidence {:?} \
(expected one use or an opposite-sense pair)",
edge.id, uses,
)));
}
} else if self.kind == BodyKind::Sheet && uses.len() == 1 {
} else if self.kind == BodyKind::Cellular {
if uses.len() < 2 {
issues.push(ValidationIssue::error_kind(IssueKind::OpenEdge,
format!("Cellular edge {} used {} times (expected at least 2)", edge.id, uses.len())));
}
} else if uses.len() != 2 {
let kind = if uses.len() < 2 { IssueKind::OpenEdge } else { IssueKind::OverUsedEdge };
issues.push(ValidationIssue::error_kind(kind, format!(
"edge {} used {} times (expected 2)",
edge.id,
uses.len()
)));
} else if uses[0] == uses[1] {
issues.push(ValidationIssue::error(format!(
"edge {} has coedges with the same sense",
edge.id
)));
}
}
for edge_id in edge_uses.keys() {
if !edges.contains_key(edge_id) {
issues.push(ValidationIssue::error(format!(
"topology references missing edge {}",
edge_id
)));
}
}
let non_degenerate_vertex_ids = self
.edges
.iter()
.filter(|edge| !edge.degenerate)
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect::<HashSet<_>>();
let vertex_count = vertices
.keys()
.filter(|id| non_degenerate_vertex_ids.contains(id))
.count() as i64;
let edge_count = self.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
let face_count = face_ids.len() as i64;
let hole_count = self.bounding_hole_count();
let shell_count = self.shells.len() as i64;
let actual = vertex_count - edge_count + face_count - hole_count;
let expected = 2 * (shell_count - self.genus);
let closed = self
.edges
.iter()
.filter(|edge| !edge.degenerate)
.all(|edge| edge_uses.get(&edge.id).map(Vec::len) == Some(2));
if !matches!(self.kind, BodyKind::Sheet | BodyKind::Cellular) && closed && actual != expected {
let pairs = self.coincident_parallel_edge_pairs() as i64;
if actual + pairs != expected {
issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch, format!(
"Euler formula: V-E+F-H = {}, expected {} ({} coincident parallel edge pair(s) credited)",
actual + pairs,
expected,
pairs
)));
}
}
if self.kind == BodyKind::Cellular {
self.validate_cells(&edges, &mut issues);
}
ValidationReport {
issues,
wire_warnings,
max_pcurve_error,
}
}
fn validate_cells(&self, edges: &HashMap<u64, &EdgeRecord>, issues: &mut Vec<ValidationIssue>) {
if self.cells.is_empty() {
issues.push(ValidationIssue::error("Cellular body has no declared cells"));
return;
}
let faces: HashMap<_, _> = self.shells.iter().flat_map(|s| &s.faces).map(|f| (f.id, f)).collect();
let mut cell_ids = HashSet::default();
let mut owners: HashMap<u64, Vec<bool>> = HashMap::default();
let mut fans: HashMap<u64, Vec<(&FaceRecord, bool, &CoedgeRecord)>> = HashMap::default();
for cell in &self.cells {
if !cell_ids.insert(cell.id) {
issues.push(ValidationIssue::error(format!("duplicate cell id {}", cell.id)));
}
if cell.faces.is_empty() {
issues.push(ValidationIssue::error(format!("cell {} has no faces", cell.id)));
}
let mut seen = HashSet::default();
let mut uses: HashMap<u64, Vec<bool>> = HashMap::default();
let mut holes = 0i64;
for &(id, forward) in &cell.faces {
if !seen.insert(id) {
issues.push(ValidationIssue::error(format!("cell {} repeats face {id}", cell.id)));
continue;
}
let Some(&face) = faces.get(&id) else {
issues.push(ValidationIssue::error(format!("cell {} references missing face {id}", cell.id)));
continue;
};
owners.entry(id).or_default().push(forward);
holes += face.loops.iter().filter(|record| !record.coedges.is_empty()
&& record.coedges.iter().any(|c| edges.get(&c.edge_id).is_some_and(|e| !e.degenerate)))
.count().saturating_sub(1) as i64;
for coedge in face.loops.iter().flat_map(|l| &l.coedges) {
uses.entry(coedge.edge_id).or_default().push(coedge.forward == forward);
fans.entry(coedge.edge_id).or_default().push((face, forward, coedge));
}
}
let mut vertices = HashSet::default();
let mut edge_count = 0i64;
for (id, senses) in &uses {
let Some(&edge) = edges.get(id) else { continue; };
let opposite = senses.len() == 2 && senses[0] != senses[1];
if !(opposite || (edge.degenerate && senses.len() == 1)) {
issues.push(ValidationIssue::error(format!(
"cell {} edge {id} is not closed with opposite senses: {senses:?}", cell.id)));
}
if !edge.degenerate {
edge_count += 1;
vertices.extend([edge.start_vertex_id, edge.end_vertex_id]);
}
}
let chi = vertices.len() as i64 - edge_count + seen.len() as i64 - holes;
if chi % 2 != 0 {
issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch,
format!("cell {} has odd Euler characteristic {chi}", cell.id)));
}
}
for id in faces.keys() {
let senses = owners.get(id).map(Vec::as_slice).unwrap_or(&[]);
if senses.is_empty() || senses.len() > 2 || (senses.len() == 2 && senses[0] == senses[1]) {
issues.push(ValidationIssue::error(format!(
"face {id} has invalid cell ownership senses {senses:?}")));
}
}
for (id, uses) in fans {
let Some(&edge) = edges.get(&id) else { continue; };
if edge.degenerate || uses.len() <= 2 { continue; }
for fraction in [0.25, 0.5, 0.75] {
let reading = (|| -> Result<Vec<(f64, bool)>, String> {
let t = edge.t0 + (edge.t1-edge.t0)*fraction;
let derivatives = edge.curve.derivatives(t, 1)?;
let tangent = derivatives[1].scale(if edge.t1 >= edge.t0 { 1.0 } else { -1.0 }).normalized()?;
let axis = tangent.perpendicular()?;
let other = tangent.cross(axis);
let mut radial = Vec::new();
for &(face, forward, coedge) in &uses {
let uv = crate::project_point_to_surface(&face.surface, derivatives[0])?;
let normal = face.surface.normal(uv.u, uv.v)?
.scale(if face.same_sense { 1.0 } else { -1.0 });
let ray = normal.cross(tangent.scale(if coedge.forward { 1.0 } else { -1.0 }));
let (x,y) = (ray.dot(axis), ray.dot(other));
if !x.is_finite() || !y.is_finite() || x*x+y*y < 1e-20 {
return Err("undefined radial face tangent".into());
}
radial.push((y.atan2(x), coedge.forward == forward));
}
radial.sort_by(|a,b| a.0.total_cmp(&b.0).then_with(|| b.1.cmp(&a.1)));
Ok(radial)
})();
match reading {
Ok(radial) if radial.iter().enumerate().all(|(i, a)| a.1 != radial[(i+1)%radial.len()].1) => {},
Ok(_) => { issues.push(ValidationIssue::error(format!(
"Cellular edge {id} radial cell senses do not alternate at {fraction}"))); break; },
Err(error) => { issues.push(ValidationIssue::error(format!(
"Cellular edge {id} radial cell fan cannot be read: {error}"))); break; },
}
}
}
}
pub(crate) fn bounding_hole_count(&self) -> i64 {
let degenerate: HashSet<u64> = self
.edges
.iter()
.filter(|edge| edge.degenerate)
.map(|edge| edge.id)
.collect();
self.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| {
face.loops
.iter()
.filter(|record| {
!record.coedges.is_empty()
&& !record
.coedges
.iter()
.all(|coedge| degenerate.contains(&coedge.edge_id))
})
.count()
.saturating_sub(1) as i64
})
.sum()
}
pub(crate) fn coincident_parallel_edge_pairs(&self) -> usize {
let candidates: Vec<&EdgeRecord> =
self.edges.iter().filter(|edge| !edge.degenerate).collect();
let mut consumed = vec![false; candidates.len()];
let mut pairs = 0usize;
for first_index in 0..candidates.len() {
if consumed[first_index] {
continue;
}
let first = candidates[first_index];
for second_index in first_index + 1..candidates.len() {
if consumed[second_index] {
continue;
}
let second = candidates[second_index];
let endpoints_match = (first.start_vertex_id == second.start_vertex_id
&& first.end_vertex_id == second.end_vertex_id)
|| (first.start_vertex_id == second.end_vertex_id
&& first.end_vertex_id == second.start_vertex_id);
if !endpoints_match {
continue;
}
let coincident = (1..4).all(|sample| {
let fraction = sample as f64 / 4.0;
let Ok(on_first) = first
.curve
.evaluate(first.t0 + (first.t1 - first.t0) * fraction)
else {
return false;
};
let forward = second.t0 + (second.t1 - second.t0) * fraction;
let backward = second.t1 - (second.t1 - second.t0) * fraction;
let tolerance = 1e-6 * (1.0 + on_first.length());
let matches_forward = second
.curve
.evaluate(forward)
.map(|point| point.sub(on_first).length() <= tolerance)
.unwrap_or(false);
let matches_backward = second
.curve
.evaluate(backward)
.map(|point| point.sub(on_first).length() <= tolerance)
.unwrap_or(false);
matches_forward || matches_backward
});
if coincident {
consumed[first_index] = true;
consumed[second_index] = true;
pairs += 1;
break;
}
}
}
pairs
}
}
fn coedge_sample(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
fraction: f64,
) -> Result<(f64, Vec3, Vec3), String> {
let [q0, q1] = pcurve.domain()?;
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
let on_surface = surface.evaluate_extended(uv.x, uv.y)?;
let t = if forward {
edge.t0 + (edge.t1 - edge.t0) * fraction
} else {
edge.t1 - (edge.t1 - edge.t0) * fraction
};
let on_curve = curve.evaluate(t)?;
Ok((on_surface.sub(on_curve).length(), on_surface, on_curve))
}
pub(crate) fn adaptive_coedge_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
tolerance: f64,
) -> Result<f64, String> {
fn interval(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
a: f64,
b: f64,
sample_a: (f64, Vec3, Vec3),
sample_b: (f64, Vec3, Vec3),
tolerance: f64,
depth: usize,
) -> Result<f64, String> {
let mid = (a + b) * 0.5;
let sample_mid = coedge_sample(surface, pcurve, curve, edge, forward, mid)?;
let error_nonlinearity = (sample_mid.0 - (sample_a.0 + sample_b.0) * 0.5).abs();
let surface_bend = sample_mid
.1
.sub(sample_a.1.add(sample_b.1).scale(0.5))
.length();
let curve_bend = sample_mid
.2
.sub(sample_a.2.add(sample_b.2).scale(0.5))
.length();
let local_max = sample_a.0.max(sample_mid.0).max(sample_b.0);
if depth >= 3
|| (error_nonlinearity <= tolerance * 0.05
&& surface_bend.max(curve_bend) <= tolerance * 0.25)
{
return Ok(local_max);
}
Ok(interval(
surface,
pcurve,
curve,
edge,
forward,
a,
mid,
sample_a,
sample_mid,
tolerance,
depth + 1,
)?
.max(interval(
surface,
pcurve,
curve,
edge,
forward,
mid,
b,
sample_mid,
sample_b,
tolerance,
depth + 1,
)?))
}
let mut maximum = 0.0f64;
let mut previous = coedge_sample(surface, pcurve, curve, edge, forward, 0.0)?;
maximum = maximum.max(previous.0);
for index in 1..=32 {
let a = (index - 1) as f64 / 32.0;
let b = index as f64 / 32.0;
let next = coedge_sample(surface, pcurve, curve, edge, forward, b)?;
maximum = maximum.max(interval(
surface, pcurve, curve, edge, forward, a, b, previous, next, tolerance, 0,
)?);
previous = next;
}
Ok(maximum)
}
thread_local! {
static COEDGE_MEMO: std::cell::RefCell<Option<HashMap<Vec<u64>, Result<f64, String>>>> =
const { std::cell::RefCell::new(None) };
static RETAINED_COEDGE_READINGS: std::cell::RefCell<(HashMap<Vec<u64>, f64>, usize)> =
std::cell::RefCell::new((HashMap::default(), 0));
static COEDGE_MEMO_COUNTS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
}
const RETAINED_COEDGE_WORDS: usize = 1 << 20;
const RETAINED_COEDGE_ENTRIES: usize = 4096;
pub(crate) fn clear_coedge_readings() {
RETAINED_COEDGE_READINGS.with(|cache| *cache.borrow_mut() = (HashMap::default(), 0));
}
fn retain_coedge_reading(key: Vec<u64>, value: f64) {
if key.len() > RETAINED_COEDGE_WORDS { return; }
RETAINED_COEDGE_READINGS.with(|cache| {
let (map, words) = &mut *cache.borrow_mut();
if *words + key.len() > RETAINED_COEDGE_WORDS || map.len() >= RETAINED_COEDGE_ENTRIES {
map.clear();
*words = 0;
}
*words += key.len();
map.insert(key, value);
});
}
pub(crate) fn coedge_memo_enabled() -> bool {
switch!(live "BREP_VALIDATE_COEDGE_MEMO", true)
}
pub(crate) struct CoedgeMemoScope {
opened: bool,
}
impl CoedgeMemoScope {
pub(crate) fn open(enabled: bool) -> Self {
let opened = enabled
&& COEDGE_MEMO.with(|memo| {
let mut memo = memo.borrow_mut();
if memo.is_some() {
return false;
}
*memo = Some(HashMap::default());
COEDGE_MEMO_COUNTS.with(|counts| counts.set((0, 0)));
true
});
CoedgeMemoScope { opened }
}
}
impl Drop for CoedgeMemoScope {
fn drop(&mut self) {
if self.opened {
COEDGE_MEMO.with(|memo| *memo.borrow_mut() = None);
let (hits, misses) = COEDGE_MEMO_COUNTS.with(|counts| counts.get());
if crate::profile::profile_enabled() {
profile_line!("validate.coedge_memo hits={hits} misses={misses}");
}
}
}
}
fn coedge_memo_key(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
tolerance: f64,
) -> Vec<u64> {
let mut key = Vec::with_capacity(
16 + surface.knots_u.len() + surface.knots_v.len() + 4 * surface.control_points.iter().map(Vec::len).sum::<usize>()
+ pcurve.knots.len() + 4 * pcurve.control_points.len() + curve.knots.len() + 4 * curve.control_points.len(),
);
let push_point = |key: &mut Vec<u64>, point: &crate::Vec4| key.extend([point.x.to_bits(), point.y.to_bits(), point.z.to_bits(), point.w.to_bits()]);
key.extend([edge.t0.to_bits(), edge.t1.to_bits(), forward as u64, tolerance.to_bits()]);
key.extend([surface.degree_u as u64, surface.degree_v as u64, surface.knots_u.len() as u64, surface.knots_v.len() as u64, surface.control_points.len() as u64]);
key.extend(surface.knots_u.iter().chain(&surface.knots_v).map(|knot| knot.to_bits()));
for row in &surface.control_points {
key.push(row.len() as u64);
for point in row {
push_point(&mut key, point);
}
}
for nurbs in [pcurve, curve] {
key.extend([nurbs.degree as u64, nurbs.knots.len() as u64, nurbs.control_points.len() as u64]);
key.extend(nurbs.knots.iter().map(|knot| knot.to_bits()));
for point in &nurbs.control_points {
push_point(&mut key, point);
}
}
key
}
pub(crate) fn exact_lines_enabled() -> bool {
switch!("BREP_VALIDATE_EXACT_LINES", true)
}
pub(crate) fn exact_line_on_plane_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Option<f64> {
let linear = |nurbs: &NurbsCurve| {
nurbs.degree == 1
&& nurbs.control_points.len() == 2
&& nurbs.knots.len() == 4
&& nurbs.control_points[0].w == nurbs.control_points[1].w
&& nurbs.control_points[0].w > 0.0
};
if !linear(pcurve) || !linear(curve) {
return None;
}
if surface.degree_u != 1
|| surface.degree_v != 1
|| surface.knots_u.len() != 4
|| surface.knots_v.len() != 4
|| surface.control_points.len() != 2
|| surface.control_points.iter().any(|row| row.len() != 2)
{
return None;
}
let weight = surface.control_points[0][0].w;
if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
return None;
}
let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
let defect = p11.sub(p10).sub(p01.sub(p00));
if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
return None;
}
let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
let [q0, q1] = pcurve.domain().ok()?;
for q in [q0, q1] {
let uv = pcurve.evaluate(q).ok()?;
if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
return None;
}
}
let [c0, c1] = curve.domain().ok()?;
let (low, high) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
if !(low >= c0 && high <= c1) {
return None;
}
let start = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
let end = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
Some(start.max(end))
}
pub(crate) fn exact_iso_image_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Option<f64> {
let weight = pcurve.control_points.first()?.w;
if pcurve.control_points.len() < 2 || !(weight > 0.0) || pcurve.control_points.iter().any(|control| control.w != weight) {
return None;
}
let points = pcurve.control_points.iter().map(|control| control.point().ok()).collect::<Option<Vec<_>>>()?;
let (a, b) = (points[0], points[points.len() - 1]);
let (along_u, constant, run) = if points.iter().all(|point| point.y == a.y) && a.x != b.x {
(true, a.y, (a.x, b.x))
} else if points.iter().all(|point| point.x == a.x) && a.y != b.y {
(false, a.x, (a.y, b.y))
} else {
return None;
};
let linear = pcurve.degree == 1 && points.len() == 2 && pcurve.knots.len() == 4;
let run = if linear {
run
} else {
let degree = pcurve.degree;
if degree == 0 || pcurve.knots.len() != points.len() + degree + 1 {
return None;
}
for (index, point) in points.iter().enumerate() {
let greville = pcurve.knots[index + 1..=index + degree].iter().sum::<f64>() / degree as f64;
if (if along_u { point.x } else { point.y }) != greville {
return None;
}
}
let [q0, q1] = pcurve.domain().ok()?;
(q0, q1)
};
let (run_knots, run_degree, other_knots, other_degree) = if along_u {
(&surface.knots_u, surface.degree_u, &surface.knots_v, surface.degree_v)
} else {
(&surface.knots_v, surface.degree_v, &surface.knots_u, surface.degree_u)
};
if curve.degree != run_degree || &curve.knots != run_knots {
return None;
}
let expected = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
if run.0 != expected.0 || run.1 != expected.1 {
return None;
}
let run_vector = crate::KnotVector::new(run_knots.clone(), run_degree).ok()?;
let [r0, r1] = run_vector.domain();
let (low, high) = (run.0.min(run.1), run.0.max(run.1));
if !(low >= r0 && high <= r1) {
return None;
}
let other = crate::KnotVector::new(other_knots.clone(), other_degree).ok()?;
let [o0, o1] = other.domain();
if !(constant >= o0 && constant <= o1) {
return None;
}
let span = other.find_span(constant);
let basis = other.basis_functions(span, constant);
let count = curve.control_points.len();
let rows = &surface.control_points;
let net_count = if along_u { rows.len() } else { rows.first()?.len() };
if net_count != count {
return None;
}
let mut controls = Vec::with_capacity(count);
for index in 0..count {
let mut blended = crate::Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
for (offset, weight) in basis.iter().enumerate() {
let other_index = span - other_degree + offset;
let control = if along_u { *rows.get(index)?.get(other_index)? } else { *rows.get(other_index)?.get(index)? };
blended = blended.add(control.scale(*weight));
}
if !(blended.w > 0.0) {
return None;
}
controls.push(blended);
}
let factor = controls[0].w / curve.control_points[0].w;
if !(factor > 0.0) {
return None;
}
let mut weight_defect = 0.0f64;
let mut gap = 0.0f64;
let mut diameter = 0.0f64;
let first = curve.control_points[0].point().ok()?;
for (iso, own) in controls.iter().zip(&curve.control_points) {
if !(own.w > 0.0) {
return None;
}
weight_defect = weight_defect.max((iso.w / factor - own.w).abs() / own.w);
let (iso_point, own_point) = (iso.point().ok()?, own.point().ok()?);
gap = gap.max(iso_point.sub(own_point).length());
diameter = diameter.max(own_point.sub(first).length());
}
if weight_defect > 1e-12 {
return None;
}
Some(gap + 4.0 * weight_defect * diameter)
}
pub(crate) fn exact_images_enabled() -> bool {
switch!(live "BREP_VALIDATE_EXACT_IMAGES", true)
}
pub(crate) fn exact_plane_image_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Option<f64> {
if pcurve.degree != curve.degree || pcurve.knots != curve.knots || pcurve.control_points.len() != curve.control_points.len() {
return None;
}
if surface.degree_u != 1
|| surface.degree_v != 1
|| surface.knots_u.len() != 4
|| surface.knots_v.len() != 4
|| surface.control_points.len() != 2
|| surface.control_points.iter().any(|row| row.len() != 2)
{
return None;
}
let weight = surface.control_points[0][0].w;
if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
return None;
}
let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
let defect = p11.sub(p10).sub(p01.sub(p00));
if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
return None;
}
let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
if !(u1 > u0 && v1 > v0) {
return None;
}
let [q0, q1] = pcurve.domain().ok()?;
let [c0, c1] = curve.domain().ok()?;
let (t0, t1) = (edge.t0, edge.t1);
let reference = if forward {
if q0.to_bits() != t0.to_bits() || q1.to_bits() != t1.to_bits() {
return None;
}
curve.clone()
} else {
if q0.to_bits() != (c0 + c1 - t1).to_bits() || q1.to_bits() != (c0 + c1 - t0).to_bits() {
return None;
}
let reversed = curve.reversed().ok()?;
if reversed.knots != pcurve.knots {
return None;
}
reversed
};
let (du, dv) = (p10.sub(p00), p01.sub(p00));
let mut image = Vec::with_capacity(pcurve.control_points.len());
for control in &pcurve.control_points {
if !(control.w > 0.0) {
return None;
}
let (u, v) = (control.x / control.w, control.y / control.w);
let point = p00.add(du.scale((u - u0) / (u1 - u0))).add(dv.scale((v - v0) / (v1 - v0)));
image.push(crate::Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w });
}
let bound = homogeneous_net_bound(&reference.control_points, &image)?;
let scale = [p00, p01, p10, p11].iter().fold(0f64, |m, p| m.max(p.length()));
(bound <= 1e-10 * (1.0 + scale)).then_some(bound)
}
fn homogeneous_net_bound(a: &[crate::Vec4], b: &[crate::Vec4]) -> Option<f64> {
if a.len() != b.len() || a.is_empty() {
return None;
}
let origin = Vec3::new(a[0].x / a[0].w, a[0].y / a[0].w, a[0].z / a[0].w);
let mut floor = f64::INFINITY;
let (mut numerator, mut denominator, mut reach) = (0f64, 0f64, 0f64);
for (p, q) in a.iter().zip(b) {
if !(p.w > 0.0 && q.w > 0.0) {
return None;
}
floor = floor.min(p.w);
let ph = Vec3::new(p.x, p.y, p.z).sub(origin.scale(p.w));
let qh = Vec3::new(q.x, q.y, q.z).sub(origin.scale(q.w));
numerator = numerator.max(ph.sub(qh).length());
denominator = denominator.max((p.w - q.w).abs());
reach = reach.max(qh.length() / q.w);
}
let bound = (numerator + reach * denominator) / floor;
bound.is_finite().then_some(bound)
}
pub(crate) fn exact_iso_enabled() -> bool {
switch!("BREP_VALIDATE_EXACT_ISO", true)
}
pub(crate) fn exact_iso_line_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Option<f64> {
if pcurve.degree != 1
|| pcurve.control_points.len() != 2
|| pcurve.knots.len() != 4
|| pcurve.control_points[0].w != pcurve.control_points[1].w
|| !(pcurve.control_points[0].w > 0.0)
{
return None;
}
let [q0, q1] = pcurve.domain().ok()?;
let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
for uv in [a, b] {
if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
return None;
}
}
let (iso, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x {
(surface.iso_curve_v(a.y).ok()?, a.x, b.x)
} else if a.x.to_bits() == b.x.to_bits() && a.y != b.y {
(surface.iso_curve_u(a.x).ok()?, a.y, b.y)
} else {
return None;
};
let (start, end) = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
if start.to_bits() != from.to_bits() || end.to_bits() != to.to_bits() {
return None;
}
let matches = |iso: &NurbsCurve| {
iso.degree == curve.degree
&& iso.knots.len() == curve.knots.len()
&& iso.knots.iter().zip(&curve.knots).all(|(x, y)| x.to_bits() == y.to_bits())
&& iso.control_points.len() == curve.control_points.len()
};
let iso = if !matches(&iso) && exact_images_enabled() {
let (low, high) = (from.min(to), from.max(to));
let [d0, d1] = iso.domain().ok()?;
let tail = if low == d0 { iso } else { iso.split(low).ok()?.1 };
let piece = if high == d1 { tail } else { tail.split(high).ok()?.0 };
if !matches(&piece) {
return None;
}
crate::blend::record_count("count.validate.exact_iso_piece", 1);
piece
} else {
iso
};
if !matches(&iso) {
return None;
}
let size = iso
.control_points
.iter()
.filter_map(|c| c.point().ok())
.fold(0f64, |m, q| m.max(q.x.abs()).max(q.y.abs()).max(q.z.abs()));
let mut deviation = 0f64;
for (p, q) in iso.control_points.iter().zip(&curve.control_points) {
if p.w.to_bits() != q.w.to_bits() || !(p.w > 0.0) {
return None;
}
deviation = deviation.max(p.point().ok()?.sub(q.point().ok()?).length());
}
if !(deviation <= 1e-12 * size.max(1.0)) {
return None;
}
let start_gap = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
let end_gap = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
Some(start_gap.max(end_gap).max(deviation))
}
fn memoized_coedge_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
tolerance: f64,
) -> Result<f64, String> {
if exact_lines_enabled() {
if let Some(error) = exact_line_on_plane_error(surface, pcurve, curve, edge, forward) {
return Ok(error);
}
}
if exact_iso_enabled() {
if let Some(error) = exact_iso_line_error(surface, pcurve, curve, edge, forward) {
crate::blend::record_count("count.validate.exact_iso", 1);
return Ok(error);
}
}
if exact_images_enabled() {
if let Some(error) = exact_plane_image_error(surface, pcurve, curve, edge, forward) {
crate::blend::record_count("count.validate.exact_image", 1);
return Ok(error);
}
}
if COEDGE_MEMO.with(|memo| memo.borrow().is_none()) {
return coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
}
let key = coedge_memo_key(surface, pcurve, curve, edge, forward, tolerance);
if let Some(hit) = COEDGE_MEMO.with(|memo| memo.borrow().as_ref().and_then(|map| map.get(&key).cloned())) {
COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
return hit;
}
if let Some(value) = RETAINED_COEDGE_READINGS.with(|cache| cache.borrow().0.get(&key).copied()) {
COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
COEDGE_MEMO.with(|memo| {
if let Some(map) = memo.borrow_mut().as_mut() { map.insert(key, Ok(value)); }
});
return Ok(value);
}
COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0, counts.get().1 + 1)));
let read = coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
if let Ok(value) = read { retain_coedge_reading(key.clone(), value); }
COEDGE_MEMO.with(|memo| {
if let Some(map) = memo.borrow_mut().as_mut() {
map.insert(key, read.clone());
}
});
read
}
pub(crate) fn coedge_error_floored(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
tolerance: f64,
) -> Result<f64, String> {
let steered = adaptive_coedge_error(surface, pcurve, curve, edge, forward, tolerance)?;
Ok(steered.max(knot_span_midpoint_error(surface, pcurve, curve, edge, forward)?))
}
fn coedge_floor_tracing() -> bool {
debug_flag!("BREP_DEBUG_COEDGE_FLOOR")
}
fn knot_span_midpoint_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Result<f64, String> {
let mut worst = 0.0_f64;
let [q0, q1] = pcurve.domain()?;
if q1 > q0 {
let mut previous = q0;
for &knot in pcurve.knots.iter().chain(std::iter::once(&q1)) {
if knot > previous && knot <= q1 {
let fraction = ((previous + knot) * 0.5 - q0) / (q1 - q0);
let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
if coedge_floor_tracing() && sample.0 > 1e-3 {
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
let ([u0, u1], [v0, v1]) = (surface.domain_u()?, surface.domain_v()?);
eprintln!(
"COEDGE-FLOOR edge {} pcurve-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6}) domain u[{u0},{u1}] v[{v0},{v1}] closed {:?} analytic {:?} degree {} knots {} cps {}",
edge.id, sample.0, uv.x, uv.y, surface.closed_directions()?, surface.analytic().map(|_| "yes"), pcurve.degree, pcurve.knots.len(), pcurve.control_points.len()
);
}
worst = worst.max(sample.0);
previous = knot;
}
}
}
let span = edge.t1 - edge.t0;
if span.is_finite() && span != 0.0 {
let low = edge.t0.min(edge.t1);
let high = edge.t0.max(edge.t1);
let mut previous = low;
for &knot in curve.knots.iter().chain(std::iter::once(&high)) {
if knot > previous && knot <= high {
let midpoint = (previous + knot) * 0.5;
let fraction = if forward {
(midpoint - edge.t0) / span
} else {
(edge.t1 - midpoint) / span
};
let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
if coedge_floor_tracing() && sample.0 > 1e-3 {
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
eprintln!(
"COEDGE-FLOOR edge {} edge-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6})",
edge.id, sample.0, uv.x, uv.y
);
}
worst = worst.max(sample.0);
previous = knot;
}
}
}
Ok(worst)
}
fn segments_cross(a: Vec2, b: Vec2, c: Vec2, d: Vec2, tolerance: f64) -> bool {
let cross = |first: Vec2, second: Vec2| first.x * second.y - first.y * second.x;
let ab = b.sub(a);
let cd = d.sub(c);
let denominator = cross(ab, cd);
if denominator.abs() <= tolerance {
return false;
}
let ac = c.sub(a);
let t = cross(ac, cd) / denominator;
let u = cross(ac, ab) / denominator;
t > tolerance && t < 1.0 - tolerance && u > tolerance && u < 1.0 - tolerance
}
fn validate_periodic_boundary_winding(
surface: &NurbsSurface,
face: &FaceRecord,
tolerances: &KernelTolerances,
) -> Option<String> {
let Some(crate::AnalyticSurface::RuledRevolution { rho0, rho1, .. }) = surface.analytic() else {
return None;
};
if rho0 * rho1 <= 0.0 || rho0.abs().min(rho1.abs()) <= tolerances.model {
return None;
}
let [u0, u1] = surface.domain_u().ok()?;
let period = u1 - u0;
if period <= 0.0 { return None; }
let mut displacement = 0.0;
for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
let [t0, t1] = coedge.pcurve.domain().ok()?;
let start = coedge.pcurve.evaluate(t0).ok()?;
let end = coedge.pcurve.evaluate(t1).ok()?;
displacement += end.x - start.x;
}
let winding = displacement / period;
(winding.abs() > 0.5).then(|| format!(
"nonzero periodic boundary winding ({winding:.6} turns) on a nonsingular revolution wall"
))
}
fn validate_uv_wire(
surface: &NurbsSurface,
loop_record: &LoopRecord,
edges: &HashMap<u64, &EdgeRecord>,
tolerances: &KernelTolerances,
outer: bool,
same_sense: bool,
) -> Option<String> {
let u_domain = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)
.ok()?
.domain();
let v_domain = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)
.ok()?
.domain();
let u_span = u_domain[1] - u_domain[0];
let v_span = v_domain[1] - v_domain[0];
let mut points = Vec::<Vec2>::new();
for coedge in &loop_record.coedges {
let edge = edges.get(&coedge.edge_id)?;
if edge.degenerate {
continue;
}
let [q0, q1] = coedge.pcurve.domain().ok()?;
for index in 0..=8 {
if !points.is_empty() && index == 0 {
continue;
}
let fraction = index as f64 / 8.0;
let parameter = q0 + (q1 - q0) * fraction;
let value = coedge.pcurve.evaluate(parameter).ok()?;
let mut point = Vec2 {
x: value.x,
y: value.y,
};
if let Some(previous) = points.last() {
while point.x - previous.x > u_span * 0.5 {
point.x -= u_span;
}
while point.x - previous.x < -u_span * 0.5 {
point.x += u_span;
}
while point.y - previous.y > v_span * 0.5 {
point.y -= v_span;
}
while point.y - previous.y < -v_span * 0.5 {
point.y += v_span;
}
}
points.push(point);
}
}
if points.len() < 4 {
return None;
}
let first = points[0];
let last = *points.last()?;
let uv_tolerance = tolerances.model.max(1e-8);
if last.sub(first).length() > uv_tolerance * 100.0 {
let du = (last.x - first.x) / u_span;
let dv = (last.y - first.y) / v_span;
if (du - du.round()).abs() * u_span > uv_tolerance * 100.0
|| (dv - dv.round()).abs() * v_span > uv_tolerance * 100.0
{
return Some(format!(
"wire is open in parameter space (gap {:.3e})",
last.sub(first).length()
));
}
}
for first_index in 0..points.len() - 1 {
for second_index in first_index + 2..points.len() - 1 {
if first_index == 0 && second_index + 1 == points.len() - 1 {
continue;
}
if segments_cross(
points[first_index],
points[first_index + 1],
points[second_index],
points[second_index + 1],
1e-10,
) {
return Some(format!(
"wire self-intersects near sampled segments {first_index} and {second_index}"
));
}
}
}
let area = points
.windows(2)
.map(|pair| pair[0].x * pair[1].y - pair[1].x * pair[0].y)
.sum::<f64>()
* 0.5;
if area.abs() > uv_tolerance * uv_tolerance {
let expected_positive = if outer { same_sense } else { !same_sense };
if (area > 0.0) != expected_positive {
return Some(format!(
"{} wire winding disagrees with face sense (signed UV area {:.6})",
if outer { "outer" } else { "inner" },
area
));
}
}
None
}
const VALIDATED_CLEAN_CAP: usize = 4096;
thread_local! {
static VALIDATED_CLEAN: std::cell::RefCell<rustc_hash::FxHashSet<(u64, u64)>> =
std::cell::RefCell::new(rustc_hash::FxHashSet::default());
}
impl BrepSolid {
pub(crate) fn clear_validate_memo() {
VALIDATED_CLEAN.with(|clean| clean.borrow_mut().clear());
}
pub(crate) fn validate_memo_key(&self) -> Option<(u64, u64)> {
use crate::mass_properties::TwoLane;
fn curve(lane: &mut TwoLane, curve: &crate::NurbsCurve) {
lane.usize(curve.degree);
lane.floats(&curve.knots);
lane.usize(curve.control_points.len());
for point in &curve.control_points {
lane.floats(&[point.x, point.y, point.z, point.w]);
}
}
fn name(lane: &mut TwoLane, name: &Option<String>) {
lane.bool(name.is_some());
if let Some(name) = name {
lane.usize(name.len());
for byte in name.bytes() {
lane.u64(u64::from(byte));
}
}
}
let mut lane = TwoLane::new();
lane.u64(self.id);
lane.u64(self.genus as u64);
lane.usize(self.kind as usize);
lane.usize(self.vertices.len());
for vertex in &self.vertices {
lane.u64(vertex.id);
lane.floats(&[vertex.point.x, vertex.point.y, vertex.point.z]);
}
lane.usize(self.edges.len());
for edge in &self.edges {
lane.u64(edge.id);
curve(&mut lane, &edge.curve);
lane.floats(&[edge.t0, edge.t1]);
lane.u64(edge.start_vertex_id);
lane.u64(edge.end_vertex_id);
lane.bool(edge.degenerate);
name(&mut lane, &edge.name);
}
lane.usize(self.shells.len());
for shell in &self.shells {
lane.u64(shell.id);
lane.usize(shell.faces.len());
for face in &shell.faces {
lane.u64(face.id);
crate::mass_properties::hash_surface(&mut lane, &face.surface);
lane.bool(face.same_sense);
name(&mut lane, &face.name);
lane.usize(face.loops.len());
for loop_record in &face.loops {
lane.u64(loop_record.id);
lane.usize(loop_record.coedges.len());
for coedge in &loop_record.coedges {
lane.u64(coedge.id);
lane.u64(coedge.edge_id);
lane.bool(coedge.forward);
curve(&mut lane, &coedge.pcurve);
}
}
}
}
lane.usize(self.cells.len());
for cell in &self.cells {
lane.u64(cell.id);
lane.bool(cell.void);
lane.usize(cell.faces.len());
for (face, sense) in &cell.faces {
lane.u64(*face);
lane.bool(*sense);
}
}
let [a, b] = lane.finish();
Some((a, b))
}
}
fn validate_memo_on() -> bool {
switch!("BREP_VALIDATE_MEMO", true)
}