#![allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn check_unknown_payloads(ir: &CadIr, findings: &mut Vec<Finding>) {
let native_unknowns = ir.all_native_unknowns().unwrap_or_default();
for record in &native_unknowns {
let Some(data) = &record.data else { continue };
let hash = Sha256::digest(data)
.iter()
.fold(String::new(), |mut acc, byte| {
use std::fmt::Write as _;
let _ = write!(acc, "{byte:02x}");
acc
});
if data.len() as u64 != record.byte_len || hash != record.sha256 {
findings.push(Finding {
check: Check::PayloadIntegrity,
severity: Severity::Error,
message: "preserved payload length or hash does not match its record".into(),
entity: Some(record.id.0.clone()),
});
}
}
}
pub(super) fn check_tessellations(ir: &CadIr, findings: &mut Vec<Finding>) {
for mesh in &ir.model.tessellations {
if mesh
.vertices
.iter()
.any(|point| !point.x.is_finite() || !point.y.is_finite() || !point.z.is_finite())
{
findings.push(Finding {
check: Check::Tessellation,
severity: Severity::Error,
message: "contains a non-finite tessellation vertex".into(),
entity: Some(mesh.id.clone()),
});
}
if mesh
.triangles
.iter()
.flatten()
.any(|index| *index as usize >= mesh.vertices.len())
{
findings.push(Finding {
check: Check::Tessellation,
severity: Severity::Error,
message: "contains an out-of-range tessellation index".into(),
entity: Some(mesh.id.clone()),
});
}
if mesh
.normals
.iter()
.any(|normal| !normal.x.is_finite() || !normal.y.is_finite() || !normal.z.is_finite())
{
findings.push(Finding {
check: Check::Tessellation,
severity: Severity::Error,
message: "contains a non-finite tessellation normal".into(),
entity: Some(mesh.id.clone()),
});
}
if mesh.channels.iter().any(|channel| {
channel.data.len() != channel.item_size as usize * channel.count as usize
}) {
findings.push(Finding {
check: Check::Tessellation,
severity: Severity::Error,
message: "contains a malformed tessellation channel".into(),
entity: Some(mesh.id.clone()),
});
}
}
}
pub(super) fn degenerate(v: &Vector3) -> bool {
v.norm() <= f64::EPSILON
}
fn variable_blend_value_valid(value: &crate::geometry::VariableBlendValue) -> bool {
use crate::geometry::VariableBlendValuePayload;
let finite = |values: &[f64]| values.iter().all(|value| value.is_finite());
match &value.payload {
VariableBlendValuePayload::TwoEnds { parameters, radii } => {
finite(parameters) && finite(radii)
}
VariableBlendValuePayload::EdgeOffset { scalars, lengths } => {
finite(scalars) && finite(lengths)
}
VariableBlendValuePayload::Functional {
parameter,
radius,
terminal,
..
} => {
parameter.is_finite()
&& radius.is_finite()
&& !matches!(terminal, crate::geometry::LoftBridgeToken::Double(v) if !v.is_finite())
}
VariableBlendValuePayload::Constant {
parameters,
radius,
nested,
..
} => finite(parameters) && radius.is_finite() && variable_blend_value_valid(nested),
VariableBlendValuePayload::Interpolated {
parameter,
radius,
points,
tail,
..
} => {
parameter.is_finite()
&& radius.is_finite()
&& tail.as_ref().is_none_or(|values| finite(values))
&& points.iter().all(|point| {
point.parameter.is_finite()
&& point.radius.is_finite()
&& finite(&point.tangents)
&& point.location.x.is_finite()
&& point.location.y.is_finite()
&& point.location.z.is_finite()
&& point.normal.x.is_finite()
&& point.normal.y.is_finite()
&& point.normal.z.is_finite()
})
}
}
}
pub(super) fn check_bounds(ir: &CadIr, findings: &mut Vec<Finding>) {
for (id, tolerance) in ir
.model
.vertices
.iter()
.map(|entity| (&entity.id.0, entity.tolerance))
.chain(
ir.model
.edges
.iter()
.map(|entity| (&entity.id.0, entity.tolerance)),
)
.chain(
ir.model
.faces
.iter()
.map(|entity| (&entity.id.0, entity.tolerance)),
)
{
if tolerance.is_some_and(nonpositive) {
findings.push(Finding {
check: Check::Tolerances,
severity: Severity::Error,
message: "topology tolerance is not positive and finite".into(),
entity: Some(id.clone()),
});
} else if tolerance.is_some_and(|value| value > 1.0e6) {
findings.push(Finding {
check: Check::Tolerances,
severity: Severity::Warning,
message: "topology tolerance is outside a sane canonical range".into(),
entity: Some(id.clone()),
});
}
}
for s in &ir.model.surfaces {
match &s.geometry {
SurfaceGeometry::Plane { normal, u_axis, .. } => {
if degenerate(normal) {
bounds_err(findings, &s.id.0, "plane normal is degenerate");
}
if degenerate(u_axis) {
bounds_err(findings, &s.id.0, "plane u axis is degenerate");
}
}
SurfaceGeometry::Cylinder {
axis,
ref_direction,
radius,
..
} => {
if degenerate(axis) {
bounds_err(findings, &s.id.0, "cylinder axis is degenerate");
}
if degenerate(ref_direction) {
bounds_err(
findings,
&s.id.0,
"cylinder reference direction is degenerate",
);
}
if nonpositive(*radius) {
bounds_err(findings, &s.id.0, "cylinder radius is not positive");
}
}
SurfaceGeometry::Cone {
axis,
ref_direction,
radius,
..
} => {
if degenerate(axis) {
bounds_err(findings, &s.id.0, "cone axis is degenerate");
}
if degenerate(ref_direction) {
bounds_err(findings, &s.id.0, "cone reference direction is degenerate");
}
if *radius < 0.0 {
bounds_err(findings, &s.id.0, "cone radius is negative");
}
}
SurfaceGeometry::Sphere {
axis,
ref_direction,
radius,
..
} => {
if degenerate(axis) {
bounds_err(findings, &s.id.0, "sphere axis is degenerate");
}
if degenerate(ref_direction) {
bounds_err(
findings,
&s.id.0,
"sphere reference direction is degenerate",
);
}
if radius.abs() <= f64::EPSILON {
bounds_err(findings, &s.id.0, "sphere radius is zero");
}
}
SurfaceGeometry::Torus {
axis,
ref_direction,
major_radius,
minor_radius,
..
} => {
if degenerate(axis) {
bounds_err(findings, &s.id.0, "torus axis is degenerate");
}
if degenerate(ref_direction) {
bounds_err(findings, &s.id.0, "torus reference direction is degenerate");
}
if nonpositive(*major_radius) || minor_radius.abs() <= f64::EPSILON {
bounds_err(
findings,
&s.id.0,
"torus major radius is not positive or minor radius is zero",
);
}
}
SurfaceGeometry::Nurbs(n) => {
let expected = (n.u_count as usize) * (n.v_count as usize);
if n.control_points.len() != expected {
bounds_err(
findings,
&s.id.0,
"NURBS surface pole count does not match u_count*v_count",
);
}
check_knots(findings, &s.id.0, &n.u_knots, "u");
check_knots(findings, &s.id.0, &n.v_knots, "v");
}
SurfaceGeometry::Unknown { .. } => {}
}
}
for procedural in &ir.model.procedural_surfaces {
if let ProceduralSurfaceDefinition::Exact {
parameter_ranges, ..
} = &procedural.definition
{
if parameter_ranges
.iter()
.any(|range| !range.iter().all(|value| value.is_finite()) || range[0] > range[1])
{
bounds_err(
findings,
&procedural.id.0,
"exact spline surface parameter ranges are invalid",
);
}
}
if let ProceduralSurfaceDefinition::Compound {
parameters,
components,
} = &procedural.definition
{
if parameters.len() != components.len()
|| parameters.iter().any(|parameter| !parameter.is_finite())
{
bounds_err(
findings,
&procedural.id.0,
"compound surface parameters and components are inconsistent",
);
}
}
if let ProceduralSurfaceDefinition::Taper {
parameter, taper, ..
} = &procedural.definition
{
let vector_finite = |vector: &Vector3| {
vector.x.is_finite() && vector.y.is_finite() && vector.z.is_finite()
};
let tail_finite = match taper {
crate::geometry::TaperSurfaceKind::Standard
| crate::geometry::TaperSurfaceKind::Orthogonal { .. } => true,
crate::geometry::TaperSurfaceKind::Edge { draft } => vector_finite(draft),
crate::geometry::TaperSurfaceKind::Shadow {
draft,
sine,
cosine,
}
| crate::geometry::TaperSurfaceKind::Swept {
draft,
sine,
cosine,
} => vector_finite(draft) && sine.is_finite() && cosine.is_finite(),
crate::geometry::TaperSurfaceKind::Ruled {
draft,
sine,
cosine,
factor,
} => {
vector_finite(draft)
&& sine.is_finite()
&& cosine.is_finite()
&& factor.is_finite()
}
};
if !parameter.is_finite() || !tail_finite {
bounds_err(
findings,
&procedural.id.0,
"taper surface parameter or subtype tail is not finite",
);
}
}
if let ProceduralSurfaceDefinition::Loft {
sections,
parameter_ranges,
bridge,
..
} = &procedural.definition
{
let ranges_valid = parameter_ranges
.iter()
.all(|range| range[0].is_finite() && range[1].is_finite() && range[0] <= range[1]);
let sections_valid =
sections
.iter()
.flat_map(|section| §ion.entries)
.all(|entry| {
entry.parameter.is_finite()
&& entry.profile.iter().all(|member| {
let table = &member.data.subdata;
let expected_rows = if table.type_code == 211 {
1
} else {
usize::try_from(table.row_count).unwrap_or(usize::MAX)
};
table.rows.len() == expected_rows
&& table.rows.iter().all(|row| {
row.parameters.iter().all(|value| value.is_finite())
&& row
.columns
.iter()
.flatten()
.all(|value| value.is_finite())
})
})
});
let bridge_valid = bridge.iter().all(|token| match token {
crate::geometry::LoftBridgeToken::Double(value) => value.is_finite(),
_ => true,
});
if !ranges_valid || !sections_valid || !bridge_valid {
bounds_err(
findings,
&procedural.id.0,
"loft construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::CompoundLoft { construction } = &procedural.definition {
let vector_finite = |vector: &Vector3| {
vector.x.is_finite() && vector.y.is_finite() && vector.z.is_finite()
};
let mut scales = construction.scales.iter().flatten().collect::<Vec<_>>();
scales.extend(construction.fifth_scale.iter().map(Box::as_ref));
let tail_valid = match &construction.tail {
crate::geometry::CompoundLoftTail::Six {
scale,
direction,
parameter_range,
..
} => {
scales.extend(scale.iter().map(Box::as_ref));
vector_finite(direction)
&& parameter_range.iter().all(|value| value.is_finite())
&& parameter_range[0] <= parameter_range[1]
}
crate::geometry::CompoundLoftTail::Seven {
first_scale,
second_scale,
direction,
..
} => {
scales.extend(first_scale.iter().map(Box::as_ref));
scales.extend(second_scale.iter().map(Box::as_ref));
vector_finite(direction)
}
crate::geometry::CompoundLoftTail::Zero { direction, .. } => match direction {
crate::geometry::CompoundLoftDirection::Vector { value } => {
vector_finite(value)
}
crate::geometry::CompoundLoftDirection::Curve { .. } => true,
},
};
let scales_valid = scales.iter().all(|scale| {
scale.members.iter().all(|member| {
let data = &member.data;
let table = &data.subdata;
let expected_rows = if table.type_code == 211 {
1
} else {
usize::try_from(table.row_count).unwrap_or(usize::MAX)
};
table.rows.len() == expected_rows
&& table.rows.iter().all(|row| {
row.parameters.iter().all(|value| value.is_finite())
&& row.columns.iter().flatten().all(|value| value.is_finite())
})
&& data.direction.as_ref().is_none_or(&vector_finite)
})
});
if !tail_valid || !scales_valid {
bounds_err(
findings,
&procedural.id.0,
"compound loft construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::G2Blend { construction } = &procedural.definition {
let direction_finite = |direction: &Vector3| {
direction.x.is_finite() && direction.y.is_finite() && direction.z.is_finite()
};
let first_shape_valid = match &construction.first_shape {
crate::geometry::G2BlendFirstShape::Full { surface, tolerance } => {
surface.is_some() == tolerance.is_some()
&& tolerance.is_none_or(|value| value.is_finite() && value >= 0.0)
}
crate::geometry::G2BlendFirstShape::None {
coefficients,
tolerance,
extension,
..
} => {
coefficients.iter().all(|value| value.is_finite())
&& tolerance.is_finite()
&& *tolerance >= 0.0
&& extension.as_ref().is_none_or(|token| match token {
crate::geometry::LoftBridgeToken::Double(value) => value.is_finite(),
_ => true,
})
}
};
let ranges_valid = construction
.parameter_ranges
.iter()
.all(|range| range[0].is_finite() && range[1].is_finite() && range[0] <= range[1]);
let scalars_valid = construction
.center_parameters
.iter()
.chain(construction.trailing_parameters.iter())
.chain(construction.discontinuities.iter().flatten())
.all(|value| value.is_finite());
if !direction_finite(&construction.first.direction)
|| !direction_finite(&construction.second.direction)
|| !first_shape_valid
|| !ranges_valid
|| !scalars_valid
{
bounds_err(
findings,
&procedural.id.0,
"G2 blend construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::VariableBlend { construction } = &procedural.definition
{
let ranges_valid = [
construction.u_range,
construction.v_range,
construction.post_range,
]
.iter()
.all(|range| range[0].is_finite() && range[1].is_finite() && range[0] <= range[1]);
let sides_valid = construction.sides.iter().all(|side| {
side.scalar.is_finite()
&& side.location.x.is_finite()
&& side.location.y.is_finite()
&& side.location.z.is_finite()
});
let values_valid = variable_blend_value_valid(&construction.first_value)
&& construction
.second_value
.as_ref()
.is_none_or(variable_blend_value_valid)
&& construction
.chamfer
.as_ref()
.is_none_or(|chamfer| variable_blend_value_valid(&chamfer.value));
let scalar_tail_valid = construction.offsets.iter().all(|value| value.is_finite())
&& construction.shape_parameter.is_finite()
&& construction.shape_length.is_finite()
&& construction
.single_radius_tail
.as_ref()
.is_none_or(|tail| {
tail.parameters.iter().all(|value| value.is_finite())
&& !matches!(tail.selector, crate::geometry::LoftBridgeToken::Double(value) if !value.is_finite())
});
if !ranges_valid || !sides_valid || !values_valid || !scalar_tail_valid {
bounds_err(
findings,
&procedural.id.0,
"variable blend construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::VertexBlend { construction } = &procedural.definition {
let point_finite = |point: &crate::math::Point3| {
point.x.is_finite() && point.y.is_finite() && point.z.is_finite()
};
let vector_finite = |vector: &Vector3| {
vector.x.is_finite() && vector.y.is_finite() && vector.z.is_finite()
};
let boundaries_valid = construction.boundaries.iter().all(|boundary| {
point_finite(&boundary.magic)
&& boundary.fullness.is_finite()
&& match &boundary.geometry {
crate::geometry::VertexBlendBoundaryGeometry::Circle {
form,
twists,
parameters,
..
} => {
matches!((*form, twists.len()), (0, 0) | (1, 1) | (3, 2))
&& twists.iter().all(&point_finite)
&& parameters.iter().all(|value| value.is_finite())
}
crate::geometry::VertexBlendBoundaryGeometry::Degenerate {
location,
normals,
} => {
point_finite(location)
&& normals
.iter()
.all(|normal| vector_finite(normal) && !degenerate(normal))
}
crate::geometry::VertexBlendBoundaryGeometry::Pcurve {
fit_tolerance,
..
} => fit_tolerance.is_finite() && *fit_tolerance >= 0.0,
crate::geometry::VertexBlendBoundaryGeometry::Plane {
normal,
parameters,
..
} => {
vector_finite(normal)
&& !degenerate(normal)
&& parameters.iter().all(|value| value.is_finite())
}
}
});
if !construction.fit_tolerance.is_finite()
|| construction.fit_tolerance < 0.0
|| !boundaries_valid
{
bounds_err(
findings,
&procedural.id.0,
"vertex blend construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::Blend {
native: Some(construction),
..
} = &procedural.definition
{
let point_finite = |point: &crate::math::Point3| {
point.x.is_finite() && point.y.is_finite() && point.z.is_finite()
};
let vector_finite = |vector: &Vector3| {
vector.x.is_finite() && vector.y.is_finite() && vector.z.is_finite()
};
let ranges_valid = [&construction.u_range, &construction.v_range]
.iter()
.all(|range| range[0].is_finite() && range[1].is_finite() && range[0] <= range[1]);
let selector_valid = match construction.radius_selector {
crate::geometry::RollingBallRadiusSelector::None => true,
crate::geometry::RollingBallRadiusSelector::Value { value } => value.is_finite(),
};
let scalars_valid = construction
.offsets
.iter()
.chain(construction.parameters.iter())
.chain(construction.discontinuities.iter().flatten())
.all(|value| value.is_finite());
let sides_valid = construction
.sides
.iter()
.all(|side| point_finite(&side.location));
let third_valid = construction
.third
.as_ref()
.is_none_or(|side| vector_finite(&side.direction));
if !ranges_valid || !selector_valid || !scalars_valid || !sides_valid || !third_valid {
bounds_err(
findings,
&procedural.id.0,
"rolling-ball blend construction payload is invalid",
);
}
}
if let ProceduralSurfaceDefinition::Offset {
distance,
extension_flags,
..
} = &procedural.definition
{
if !distance.is_finite()
|| !matches!(
extension_flags.as_slice(),
[] | [false] | [true, _] | [true, _, _]
)
{
bounds_err(
findings,
&procedural.id.0,
"offset spline surface distance or extension flags are invalid",
);
}
}
}
for c in &ir.model.curves {
match &c.geometry {
CurveGeometry::Line { direction, .. } => {
if degenerate(direction) {
bounds_err(findings, &c.id.0, "line direction is degenerate");
}
}
CurveGeometry::Circle { axis, radius, .. } => {
if degenerate(axis) {
bounds_err(findings, &c.id.0, "circle axis is degenerate");
}
if nonpositive(*radius) {
bounds_err(findings, &c.id.0, "circle radius is not positive");
}
}
CurveGeometry::Ellipse {
major_radius,
minor_radius,
..
} => {
if nonpositive(*major_radius) || nonpositive(*minor_radius) {
bounds_err(findings, &c.id.0, "ellipse radius is not positive");
}
}
CurveGeometry::Parabola {
axis,
major_direction,
focal_distance,
..
} => {
if degenerate(axis) || degenerate(major_direction) {
bounds_err(findings, &c.id.0, "parabola frame is degenerate");
}
if nonpositive(*focal_distance) {
bounds_err(findings, &c.id.0, "parabola focal distance is not positive");
}
}
CurveGeometry::Hyperbola {
axis,
major_direction,
major_radius,
minor_radius,
..
} => {
if degenerate(axis) || degenerate(major_direction) {
bounds_err(findings, &c.id.0, "hyperbola frame is degenerate");
}
if nonpositive(*major_radius) || nonpositive(*minor_radius) {
bounds_err(findings, &c.id.0, "hyperbola radius is not positive");
}
}
CurveGeometry::Degenerate { point } => {
if !point.x.is_finite() || !point.y.is_finite() || !point.z.is_finite() {
bounds_err(findings, &c.id.0, "degenerate curve point is not finite");
}
}
CurveGeometry::Nurbs(n) => {
if n.control_points.len() < (n.degree as usize + 1) {
bounds_err(
findings,
&c.id.0,
"NURBS curve has too few poles for its degree",
);
}
check_knots(findings, &c.id.0, &n.knots, "");
}
CurveGeometry::Unknown { .. } => {}
}
}
for procedural in &ir.model.procedural_curves {
if let ProceduralCurveDefinition::Deformable { data, .. } = &procedural.definition {
if let crate::geometry::DeformableCurveData::VectorField {
vectors,
parameter_pairs,
} = data
{
let vectors_finite = vectors.iter().all(|vector| {
vector.x.is_finite() && vector.y.is_finite() && vector.z.is_finite()
});
let pairs_finite = parameter_pairs
.iter()
.flatten()
.all(|value| value.is_finite());
if !vectors_finite || !pairs_finite {
bounds_err(
findings,
&procedural.id.0,
"deformable vector-field payload is not finite",
);
}
}
continue;
}
if let ProceduralCurveDefinition::Spring {
context,
surface_parameter_ranges,
first_pcurve_parameter_range,
..
} = &procedural.definition
{
let surface_ranges_valid =
surface_parameter_ranges
.iter()
.enumerate()
.all(|(side, ranges)| {
ranges.is_some() == context.sides[side].surface.is_none()
&& ranges.is_none_or(|ranges| {
ranges.into_iter().all(|range| {
range.iter().all(|value| value.is_finite())
&& range[0] <= range[1]
})
})
});
let first_pcurve_range_valid = first_pcurve_parameter_range.is_some()
== context.sides[0].pcurve.is_none()
&& first_pcurve_parameter_range.is_none_or(|range| {
range.iter().all(|value| value.is_finite()) && range[0] <= range[1]
});
if !support_context_is_finite(context)
|| !surface_ranges_valid
|| !first_pcurve_range_valid
{
bounds_err(
findings,
&procedural.id.0,
"spring context or conditional null-support ranges are invalid",
);
}
continue;
}
if let ProceduralCurveDefinition::SurfaceOffset {
context,
base_u_range,
base_v_range,
base_range,
distance,
shift,
scale,
..
} = &procedural.definition
{
let ranges = [base_u_range, base_v_range, base_range];
if !support_context_is_finite(context)
|| ranges.iter().any(|range| {
!range.iter().all(|value| value.is_finite()) || range[0] > range[1]
})
|| !distance.is_finite()
|| !shift.is_finite()
|| !scale.is_finite()
{
bounds_err(
findings,
&procedural.id.0,
"surface-offset fields are not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::Silhouette {
context,
silhouette,
light_direction,
..
} = &procedural.definition
{
let draft_finite = match silhouette {
crate::geometry::SilhouetteKind::Taper { draft_factor } => draft_factor.is_finite(),
_ => true,
};
if !support_context_is_finite(context)
|| !light_direction.x.is_finite()
|| !light_direction.y.is_finite()
|| !light_direction.z.is_finite()
|| light_direction.norm() <= f64::EPSILON
|| !draft_finite
{
bounds_err(
findings,
&procedural.id.0,
"silhouette fields are not finite or the light direction is degenerate",
);
}
continue;
}
if let ProceduralCurveDefinition::SurfaceCurve { context, .. } = &procedural.definition {
if !support_context_is_finite(context) {
bounds_err(
findings,
&procedural.id.0,
"surface-curve context is not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::ThreeSurfaceIntersection { context, .. } =
&procedural.definition
{
if !support_context_is_finite(context) {
bounds_err(
findings,
&procedural.id.0,
"three-surface intersection context is not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::Projection { context, tail, .. } = &procedural.definition
{
let tail_finite = match tail {
crate::geometry::ProjectionTail::EarlyClose { .. } => true,
crate::geometry::ProjectionTail::Ranged {
parameter_range, ..
} => {
parameter_range.iter().all(|value| value.is_finite())
&& parameter_range[0] <= parameter_range[1]
}
};
if !support_context_is_finite(context) || !tail_finite {
bounds_err(
findings,
&procedural.id.0,
"projection fields are not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::Intersection { context } = &procedural.definition {
if !support_context_is_finite(context) {
bounds_err(
findings,
&procedural.id.0,
"intersection support context is not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::TwoSidedOffset { context, offsets } =
&procedural.definition
{
let finite =
support_context_is_finite(context) && offsets.iter().all(|value| value.is_finite());
if !finite {
bounds_err(
findings,
&procedural.id.0,
"two-sided offset fields are not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::Compound {
parameters,
component_parameters,
components,
} = &procedural.definition
{
if components.is_empty() || component_parameters.len() != components.len() {
bounds_err(
findings,
&procedural.id.0,
"compound components are empty or do not match component parameters",
);
}
if parameters
.iter()
.chain(component_parameters)
.any(|value| !value.is_finite())
{
bounds_err(
findings,
&procedural.id.0,
"compound parameters are not finite",
);
}
continue;
}
if let ProceduralCurveDefinition::Subset {
parameter_range, ..
} = &procedural.definition
{
if !parameter_range.iter().all(|value| value.is_finite())
|| parameter_range[0] > parameter_range[1]
{
bounds_err(
findings,
&procedural.id.0,
"subset-curve range is not finite and ordered",
);
}
continue;
}
if let ProceduralCurveDefinition::VectorOffset {
parameter_range,
offset,
..
} = &procedural.definition
{
if !parameter_range.iter().all(|value| value.is_finite())
|| parameter_range[0] > parameter_range[1]
|| !offset.x.is_finite()
|| !offset.y.is_finite()
|| !offset.z.is_finite()
{
bounds_err(
findings,
&procedural.id.0,
"vector-offset fields are not finite and ordered",
);
}
continue;
}
let ProceduralCurveDefinition::Helix {
angle_range,
center,
major,
minor,
pitch,
apex_factor,
axis,
} = &procedural.definition
else {
continue;
};
let finite = angle_range.iter().all(|value| value.is_finite())
&& center.x.is_finite()
&& center.y.is_finite()
&& center.z.is_finite()
&& [major, minor, pitch, axis]
.into_iter()
.flat_map(|vector| [vector.x, vector.y, vector.z])
.all(f64::is_finite)
&& apex_factor.is_finite();
if !finite || angle_range[0] > angle_range[1] {
bounds_err(
findings,
&procedural.id.0,
"helix fields are not finite and ordered",
);
}
if degenerate(major) || degenerate(minor) || degenerate(axis) {
bounds_err(findings, &procedural.id.0, "helix frame is degenerate");
}
if (major.norm() - minor.norm()).abs() > 1e-9 {
bounds_err(
findings,
&procedural.id.0,
"helix major and minor radii differ",
);
}
}
}
fn support_context_is_finite(context: &crate::geometry::IntcurveSupportContext) -> bool {
context
.parameter_range
.iter()
.all(|value| value.is_finite())
&& context.parameter_range[0] <= context.parameter_range[1]
&& context
.discontinuities
.iter()
.flatten()
.all(|value| value.is_finite())
}
pub(super) fn check_knots(findings: &mut Vec<Finding>, id: &str, knots: &[f64], dir: &str) {
if knots.windows(2).any(|w| w[1] < w[0]) {
let label = if dir.is_empty() {
"knot vector is not non-decreasing".to_string()
} else {
format!("{dir}-knot vector is not non-decreasing")
};
bounds_err(findings, id, &label);
}
}
pub(super) fn bounds_err(findings: &mut Vec<Finding>, id: &str, msg: &str) {
findings.push(Finding {
check: Check::Bounds,
severity: Severity::Error,
message: msg.to_string(),
entity: Some(id.to_string()),
});
}