use crate::document::CadIr;
use crate::eval::admission::{EvaluationAdmission, ModelEvaluationPass};
use crate::eval::curve_parameter_near_point;
use crate::eval::model_curve_point_by_id;
use crate::eval::model_surface_partials_by_id;
use crate::eval::model_surface_point_by_id;
use crate::eval::nurbs_pcurve_parameter_domain;
use crate::eval::pcurve_tangent;
use crate::eval::EvaluationFailure;
use crate::features::{FinitePoint3, FiniteVector3};
use crate::geometry::{
pcurve::PcurveGeometry, SolvedCurveGeometry, SolvedSurfaceGeometry, SurfaceGeometry,
};
use crate::math::{Point3, Vector3};
use crate::report::{
check::{Check, Finding},
Severity,
};
use crate::scalar::{ExtendedReal, FiniteReal};
use crate::topology::{ParameterInterval, Sense};
use crate::units::COINCIDENCE_TOLERANCE;
use cadmpeg_core::decode::{DecodeContext, ResourceLimit};
use cadmpeg_core::CodecError;
use super::pcurve_parameter_domain;
use crate::index::identities::BorrowedIdentities;
fn measured_point(
evaluation: Result<FinitePoint3, EvaluationFailure<Point3>>,
) -> Result<Option<Point3>, ResourceLimit> {
match evaluation {
Ok(point) => Ok(Some(point.get())),
Err(failure) => failure.non_finite(),
}
}
#[derive(Clone, Copy)]
struct PassEvaluator<'pass, 'model> {
index: &'pass crate::index::ModelIndex<'model>,
pass: &'pass ModelEvaluationPass,
}
impl PassEvaluator<'_, '_> {
fn curve_point(
self,
ctx: &DecodeContext<'_>,
curve: &crate::ids::CurveId,
parameter: f64,
) -> Result<FinitePoint3, EvaluationFailure<Point3>> {
self.pass
.evaluate(EvaluationAdmission::Decode(ctx), |admission| {
model_curve_point_by_id(admission, self.index, curve, parameter)
})
}
fn surface_point(
self,
ctx: &DecodeContext<'_>,
surface: &crate::ids::SurfaceId,
u: f64,
v: f64,
) -> Result<FinitePoint3, EvaluationFailure<Point3>> {
self.pass
.evaluate(EvaluationAdmission::Decode(ctx), |admission| {
model_surface_point_by_id(admission, self.index, surface, u, v)
})
}
fn surface_partials(
self,
ctx: &DecodeContext<'_>,
surface: &crate::ids::SurfaceId,
u: f64,
v: f64,
) -> Result<crate::eval::SurfacePartials<FinitePoint3, FiniteVector3>, EvaluationFailure<Point3>>
{
self.pass
.evaluate(EvaluationAdmission::Decode(ctx), |admission| {
model_surface_partials_by_id(admission, self.index, surface, u, v)
})
}
}
fn worse_mismatch(first: f64, second: f64) -> f64 {
if first.is_nan() || second.is_nan() {
f64::NAN
} else {
first.max(second)
}
}
fn allowance(document_tolerance: crate::scalar::PositiveLength, tolerances: &[Option<f64>]) -> f64 {
tolerances.iter().flatten().copied().fold(
COINCIDENCE_TOLERANCE.max(document_tolerance.get()),
f64::max,
)
}
fn procedural_support_allowance(
document_tolerance: crate::scalar::PositiveLength,
cache_fit_tolerance: Option<f64>,
) -> f64 {
COINCIDENCE_TOLERANCE + allowance(document_tolerance, &[cache_fit_tolerance])
}
pub(super) fn check_procedural_support_consistency(
ctx: &DecodeContext<'_>,
index: &crate::index::ModelIndex<'_>,
evaluations: &ModelEvaluationPass,
findings: &mut Vec<Finding>,
) -> Result<(), CodecError> {
let ir = index.ir();
let model = PassEvaluator {
index,
pass: evaluations,
};
let curves = BorrowedIdentities::build(|add| {
for curve in &ir.model.curves {
add(curve.id.as_str(), &curve.geometry);
}
});
let owners = curve_owners(ir);
for procedural in &ir.model.procedural_curves {
let Some(owner) = owners.get_unique(procedural.id.as_str()).copied() else {
continue;
};
if let crate::geometry::ProceduralCurveDefinition::TolerantIntersection {
construction: intersection,
parameterization: Some(parameterization),
..
} = procedural.definition()
{
let endpoints = intersection.endpoints();
let tolerance = intersection.tolerance().get();
let evaluated = parameterization
.parameter_range()
.endpoints()
.map(|parameter| model.curve_point(ctx, owner, parameter));
if evaluated
.iter()
.any(|evaluation| matches!(evaluation, Err(EvaluationFailure::WorkExhausted)))
{
continue;
}
let [start, end] = evaluated.map(measured_point);
let [Some(start), Some(end)] = [start?, end?] else {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(procedural.id.as_str()),
format_args!(
"charted tolerant intersection does not evaluate at both endpoints"
),
);
continue;
};
let mismatch = worse_mismatch(
Point3::distance(start, endpoints[0].get()),
Point3::distance(end, endpoints[1].get()),
);
if !mismatch.is_finite() || mismatch > tolerance {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(procedural.id.as_str()),
format_args!(
"charted tolerant intersection misses its endpoint witnesses by \
{mismatch:.6}"
),
);
}
continue;
}
if let crate::geometry::ProceduralCurveDefinition::SurfaceOffset(definition_payload) =
procedural.definition()
{
let context = definition_payload.context();
let base = definition_payload.base();
let base_endpoints = definition_payload.base_endpoints();
let offset = definition_payload.distance().get();
let Some(solved) = curves.get(owner.as_str()) else {
continue;
};
let solved = context.parameter_range().endpoints().map(|parameter| {
measured_point(crate::eval::decode::curve_point(ctx, solved, parameter))
});
let [Some(solved_start), Some(solved_end)] = [solved[0]?, solved[1]?] else {
continue;
};
let bound = procedural_support_allowance(
ir.tolerances.linear,
procedural
.cache_fit_tolerance()
.map(crate::geometry::FitTolerance::get),
);
let Some(base) = curves.get(base.as_str()) else {
continue;
};
let base = base_endpoints.map(|parameter| match parameter {
Some(parameter) => {
measured_point(crate::eval::decode::curve_point(ctx, base, parameter.get()))
}
None => Ok(None),
});
let [Some(base_start), Some(base_end)] = [base[0]?, base[1]?] else {
check_support_sides(
ctx,
(context, None),
SupportEndpointContract::Offset {
endpoints: [solved_start, solved_end],
distance: offset.abs(),
},
model,
bound,
procedural.id.as_str(),
findings,
)?;
continue;
};
let offset_mismatch = worse_mismatch(
(Point3::distance(solved_start, base_start) - offset.abs()).abs(),
(Point3::distance(solved_end, base_end) - offset.abs()).abs(),
);
if !offset_mismatch.is_finite() || offset_mismatch > bound {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(procedural.id.as_str()),
format_args!(
"surface-offset solved curve misses its base offset distance by \
{offset_mismatch:.6}"
),
);
}
check_support_sides(
ctx,
(context, None),
SupportEndpointContract::Coincident([base_start, base_end]),
model,
bound,
procedural.id.as_str(),
findings,
)?;
continue;
}
let (context, third) = match procedural.definition() {
crate::geometry::ProceduralCurveDefinition::Law { context, .. } => {
(std::borrow::Cow::Borrowed(context), None)
}
crate::geometry::ProceduralCurveDefinition::Intersection { context, .. } => {
(std::borrow::Cow::Borrowed(context), None)
}
crate::geometry::ProceduralCurveDefinition::Silhouette(definition_payload) => (
std::borrow::Cow::Borrowed(definition_payload.context()),
None,
),
crate::geometry::ProceduralCurveDefinition::Projection(definition_payload) => {
let context = definition_payload.context();
(std::borrow::Cow::Borrowed(context), None)
}
crate::geometry::ProceduralCurveDefinition::TwoSidedOffset(definition_payload) => {
let context = definition_payload.context();
{
(std::borrow::Cow::Borrowed(context), None)
}
}
crate::geometry::ProceduralCurveDefinition::SurfaceCurve { family } => {
(std::borrow::Cow::Borrowed(family.context()), None)
}
crate::geometry::ProceduralCurveDefinition::Spring(definition_payload) => (
std::borrow::Cow::Borrowed(definition_payload.support_context()),
None,
),
crate::geometry::ProceduralCurveDefinition::ThreeSurfaceIntersection(
definition_payload,
) => {
let context = definition_payload.context();
let third = definition_payload.third();
(std::borrow::Cow::Borrowed(context), Some(third))
}
_ => continue,
};
let Some(curve) = curves.get(owner.as_str()) else {
continue;
};
let solved = context.parameter_range().endpoints().map(|parameter| {
measured_point(crate::eval::decode::curve_point(ctx, curve, parameter))
});
let [Some(solved_start), Some(solved_end)] = [solved[0]?, solved[1]?] else {
continue;
};
let bound = procedural_support_allowance(
ir.tolerances.linear,
procedural
.cache_fit_tolerance()
.map(crate::geometry::FitTolerance::get),
);
check_support_sides(
ctx,
(&context, third),
SupportEndpointContract::Coincident([solved_start, solved_end]),
model,
bound,
procedural.id.as_str(),
findings,
)?;
}
Ok(())
}
#[derive(Clone, Copy)]
enum SupportEndpointContract {
Coincident([Point3; 2]),
Offset {
endpoints: [Point3; 2],
distance: f64,
},
}
fn check_support_sides(
ctx: &DecodeContext<'_>,
supports: (
&crate::geometry::IntcurveSupportContext,
Option<&crate::geometry::IntcurveSupportSide>,
),
contract: SupportEndpointContract,
model: PassEvaluator<'_, '_>,
bound: f64,
entity: &str,
findings: &mut Vec<Finding>,
) -> Result<(), CodecError> {
let (context, third) = supports;
let (constrained, expected_distance) = match contract {
SupportEndpointContract::Coincident(endpoints) => (endpoints, None),
SupportEndpointContract::Offset {
endpoints,
distance,
} => (endpoints, Some(distance)),
};
for (side_index, side) in context.sides().iter().chain(third).enumerate() {
let (Some(surface_id), Some(pcurve)) = (&side.surface, &side.pcurve) else {
continue;
};
let support = context.parameter_range().endpoints().map(
|parameter| -> Result<Option<Point3>, ResourceLimit> {
let Some(parameter) = side.pcurve_parameter(context.parameter_range(), parameter)
else {
return Ok(None);
};
let uv =
match crate::eval::decode::pcurve_uv(ctx, &pcurve.geometry, parameter.get()) {
Ok(uv) => uv.get(),
Err(failure) => {
let Some(uv) = failure.non_finite()? else {
return Ok(None);
};
uv
}
};
match model.surface_point(ctx, surface_id, uv.u, uv.v) {
Ok(point) => Ok(Some(point.get())),
Err(failure) => failure.non_finite(),
}
},
);
let [Some(support_start), Some(support_end)] = [support[0]?, support[1]?] else {
continue;
};
let endpoint_mismatch = |constrained, support| {
let distance = Point3::distance(constrained, support);
expected_distance.map_or(distance, |expected| (distance - expected).abs())
};
let mismatch = worse_mismatch(
endpoint_mismatch(constrained[0], support_start),
endpoint_mismatch(constrained[1], support_end),
);
if !mismatch.is_finite() || mismatch > bound {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(entity),
format_args!(
"procedural support side {side_index} misses its endpoint distance contract by \
{mismatch:.6}"
),
);
}
}
Ok(())
}
fn curve_owners(ir: &CadIr) -> BorrowedIdentities<'_, &crate::ids::CurveId> {
BorrowedIdentities::build(|add| {
for curve in &ir.model.curves {
if let Some(construction) = curve.geometry.procedural_construction() {
add(construction.as_str(), &curve.id);
}
}
})
}
fn vertex_positions(ir: &CadIr) -> BorrowedIdentities<'_, (Point3, Option<f64>)> {
let points = BorrowedIdentities::build(|add| {
for point in &ir.model.points {
add(point.id.as_str(), point.position().get());
}
});
BorrowedIdentities::build(|add| {
for vertex in &ir.model.vertices {
if let Some(position) = points.get(vertex.point.as_str()) {
add(
vertex.id.as_str(),
(
*position,
vertex.tolerance.map(crate::scalar::PositiveReal::get),
),
);
}
}
})
}
pub(super) fn check_edge_endpoint_consistency(
ctx: &DecodeContext<'_>,
ir: &CadIr,
findings: &mut Vec<Finding>,
) -> Result<(), CodecError> {
let curves = BorrowedIdentities::build(|add| {
for curve in &ir.model.curves {
add(curve.id.as_str(), &curve.geometry);
}
});
let owners = curve_owners(ir);
let curve_cache_tolerances = BorrowedIdentities::build(|add| {
for curve in &ir.model.procedural_curves {
if let Some(owner) = owners.get_unique(curve.id.as_str()) {
add(owner.as_str(), curve.cache_fit_tolerance());
}
}
});
let vertices = vertex_positions(ir);
for edge in &ir.model.edges {
let Some([start_t, end_t]) = edge.param_range().map(crate::units::FiniteVector::get) else {
continue;
};
let Some(curve_id) = edge.curve() else {
continue;
};
let Some(geometry) = curves.get(curve_id.as_str()) else {
continue;
};
let (Some((start, start_tol)), Some((end, end_tol))) = (
vertices.get(edge.start.as_str()),
vertices.get(edge.end.as_str()),
) else {
continue;
};
let (Some(at_start), Some(at_end)) = (
measured_point(crate::eval::decode::curve_point(ctx, geometry, start_t))?,
measured_point(crate::eval::decode::curve_point(ctx, geometry, end_t))?,
) else {
continue;
};
let bound = allowance(
ir.tolerances.linear,
&[
edge.tolerance.map(crate::scalar::PositiveReal::get),
*start_tol,
*end_tol,
curve_cache_tolerances
.get(curve_id.as_str())
.copied()
.flatten()
.map(crate::geometry::FitTolerance::get),
],
);
let mismatch = worse_mismatch(
Point3::distance(at_start, *start),
Point3::distance(at_end, *end),
);
if !mismatch.is_finite() || mismatch > bound {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(edge.id.as_str()),
format_args!(
"edge curve endpoints miss the edge's vertex positions by {mismatch:.6}"
),
);
}
}
let edges = BorrowedIdentities::build(|add| {
for edge in &ir.model.edges {
add(edge.id.as_str(), edge);
}
});
for coedge in &ir.model.coedges {
let Some(use_curve) = &coedge.use_curve else {
continue;
};
let [start_t, end_t] = use_curve.parameter_range.endpoints();
let curve_id = &use_curve.curve;
let Some(geometry) = curves.get(curve_id.as_str()) else {
continue;
};
let Some(edge) = edges.get(coedge.edge.as_str()) else {
continue;
};
let (first_vertex, last_vertex) = match coedge.sense {
Sense::Forward => (&edge.start, &edge.end),
Sense::Reversed => (&edge.end, &edge.start),
};
let (Some((start, start_tol)), Some((end, end_tol))) = (
vertices.get(first_vertex.as_str()),
vertices.get(last_vertex.as_str()),
) else {
continue;
};
let (Some(at_start), Some(at_end)) = (
measured_point(crate::eval::decode::curve_point(ctx, geometry, start_t))?,
measured_point(crate::eval::decode::curve_point(ctx, geometry, end_t))?,
) else {
continue;
};
let bound = allowance(
ir.tolerances.linear,
&[
edge.tolerance.map(crate::scalar::PositiveReal::get),
*start_tol,
*end_tol,
curve_cache_tolerances
.get(curve_id.as_str())
.copied()
.flatten()
.map(crate::geometry::FitTolerance::get),
],
);
let mismatch = worse_mismatch(
Point3::distance(at_start, *start),
Point3::distance(at_end, *end),
);
if !mismatch.is_finite() || mismatch > bound {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(coedge.id.as_str()),
format_args!(
"coedge use-curve endpoints miss the traversal vertices by {mismatch:.6}"
),
);
}
}
Ok(())
}
pub(super) fn check_pcurve_surface_consistency(
ctx: &DecodeContext<'_>,
index: &crate::index::ModelIndex<'_>,
evaluations: &ModelEvaluationPass,
findings: &mut Vec<Finding>,
) -> Result<(), CodecError> {
let ir = index.ir();
let model = PassEvaluator {
index,
pass: evaluations,
};
let curves = BorrowedIdentities::build(|add| {
for curve in &ir.model.curves {
add(curve.id.as_str(), &curve.geometry);
}
});
let surfaces = BorrowedIdentities::build(|add| {
for surface in &ir.model.surfaces {
add(surface.id.as_str(), &surface.geometry);
}
});
let surface_owners = BorrowedIdentities::build(|add| {
for surface in &ir.model.surfaces {
if let Some(construction) = surface.geometry.procedural_construction() {
add(construction.as_str(), &surface.id);
}
}
});
let procedurally_parameterized_surfaces = BorrowedIdentities::build(|add| {
for surface in &ir.model.procedural_surfaces {
if !matches!(
surface.definition(),
crate::geometry::ProceduralSurfaceDefinition::Subset(_)
) {
if let Some(owner) = surface_owners.get_unique(surface.id.as_str()) {
add(owner.as_str(), ());
}
}
}
});
let pcurves = BorrowedIdentities::build(|add| {
for pcurve in &ir.model.pcurves {
add(pcurve.id.as_str(), pcurve);
}
});
let edges = BorrowedIdentities::build(|add| {
for edge in &ir.model.edges {
add(edge.id.as_str(), edge);
}
});
let faces = BorrowedIdentities::build(|add| {
for face in &ir.model.faces {
add(face.id.as_str(), face);
}
});
let loops = BorrowedIdentities::build(|add| {
for lp in &ir.model.loops {
add(lp.id.as_str(), lp);
}
});
let vertices = vertex_positions(ir);
for coedge in &ir.model.coedges {
let Some((first_use, last_use)) = coedge.pcurves.first().zip(coedge.pcurves.last()) else {
continue;
};
let (Some(first), Some(last)) = (
pcurves.get(first_use.pcurve.as_str()),
pcurves.get(last_use.pcurve.as_str()),
) else {
continue;
};
let face = match loops.get(coedge.owner_loop.as_str()) {
Some(lp) => faces.get(lp.face.as_str()),
None => None,
};
let Some(face) = face else {
continue;
};
let Some(geometry) = surfaces.get(face.surface.as_str()) else {
continue;
};
if procedurally_parameterized_surfaces.contains(face.surface.as_str()) {
continue;
}
let Some(edge) = edges.get(coedge.edge.as_str()) else {
continue;
};
let (Some((start, start_tol)), Some((end, end_tol))) = (
vertices.get(edge.start.as_str()),
vertices.get(edge.end.as_str()),
) else {
continue;
};
let curve_geometry = match edge.curve() {
Some(curve) => curves.get(curve.as_str()).copied(),
None => None,
};
let bound = allowance(
ir.tolerances.linear,
&[
edge.tolerance.map(crate::scalar::PositiveReal::get),
*start_tol,
*end_tol,
face.tolerance.map(crate::scalar::PositiveReal::get),
first
.fit_tolerance()
.map(crate::geometry::FitTolerance::get),
last.fit_tolerance().map(crate::geometry::FitTolerance::get),
],
);
let recovery_bound = allowance(
ir.tolerances.linear,
&[
edge.tolerance.map(crate::scalar::PositiveReal::get),
*start_tol,
*end_tol,
face.tolerance.map(crate::scalar::PositiveReal::get),
],
);
let surface_context = SurfacePcurveContext {
model,
surface_id: &face.surface,
geometry,
};
let declared = if coedge.pcurves.len() == 1 {
pcurve_parameter_ranges(
ctx,
first,
first_use
.parameter_range
.map(crate::geometry::DirectedParameterRange::endpoints),
edge.param_range().map(crate::units::FiniteVector::get),
)?
} else {
let first_range = first_use
.parameter_range
.map(crate::geometry::DirectedParameterRange::endpoints)
.or(first.parameter_range().map(crate::units::FiniteVector::get));
let first_range = match first_range {
Some(range) => Some(range),
None => pcurve_parameter_extremes(ctx, first)?,
};
let last_range = last_use
.parameter_range
.map(crate::geometry::DirectedParameterRange::endpoints)
.or(last.parameter_range().map(crate::units::FiniteVector::get));
let last_range = match last_range {
Some(range) => Some(range),
None => pcurve_parameter_extremes(ctx, last)?,
};
match (first_range, last_range) {
(Some([t0, _]), Some([_, t1])) => Some(vec![[t0, t1]]),
_ => None,
}
};
let mut minimum_mismatch: Option<f64> = None;
let mut qualifies = |ranges: &Option<Vec<[f64; 2]>>| -> Result<bool, CodecError> {
for [t0, t1] in ranges.iter().flat_map(|ranges| ranges.iter()).copied() {
let pcurve_point = |geometry, parameter| match crate::eval::decode::pcurve_uv(
ctx, geometry, parameter,
) {
Ok(uv) => Ok(Some(uv.get())),
Err(failure) => failure.non_finite(),
};
let surface_point = |uv: crate::math::Point2| match model.surface_point(
ctx,
&face.surface,
uv.u,
uv.v,
) {
Ok(point) => Ok(Some(point.get())),
Err(failure) => failure.non_finite(),
};
let (Some(uv0), Some(uv1)) = (
pcurve_point(&first.geometry, t0)?,
pcurve_point(&last.geometry, t1)?,
) else {
continue;
};
let (Some(p0), Some(p1)) = (surface_point(uv0)?, surface_point(uv1)?) else {
continue;
};
let forward =
worse_mismatch(Point3::distance(p0, *start), Point3::distance(p1, *end));
let reversed =
worse_mismatch(Point3::distance(p0, *end), Point3::distance(p1, *start));
let mismatch = forward.min(reversed);
minimum_mismatch =
Some(minimum_mismatch.map_or(mismatch, |minimum| minimum.min(mismatch)));
if mismatch.is_finite() && mismatch <= bound {
return Ok(true);
}
}
Ok(false)
};
if !qualifies(&declared)? {
let recovered = edge_pcurve_parameter_ranges(
ctx,
&surface_context,
curve_geometry,
[*start, *end],
first,
last,
recovery_bound,
)?;
qualifies(&recovered)?;
}
let Some(mismatch) = minimum_mismatch else {
continue;
};
if !mismatch.is_finite() || mismatch > bound {
super::record_finding(
findings,
Check::GeometricConsistency,
Severity::Error,
Some(coedge.id.as_str()),
format_args!(
"pcurve mapped through the face surface misses the edge's vertex positions \
by {mismatch:.6}"
),
);
}
}
Ok(())
}
fn pcurve_parameter_ranges(
ctx: &DecodeContext<'_>,
pcurve: &crate::geometry::pcurve::Pcurve,
pcurve_range: Option<[f64; 2]>,
edge_range: Option<[f64; 2]>,
) -> Result<Option<Vec<[f64; 2]>>, CodecError> {
let mut ranges = Vec::new();
if let Some(range) = pcurve_range.or(pcurve
.parameter_range()
.map(crate::units::FiniteVector::get))
{
ranges.push(range);
}
if let Some([start, end]) = edge_range {
ranges.extend([[start, end], [-start, -end]]);
}
ranges.extend(pcurve_parameter_extremes(ctx, pcurve)?);
if !ranges.is_empty() {
if let Some(domain) = pcurve_parameter_domain(ctx, &pcurve.geometry)? {
ranges.push(domain.endpoints());
}
}
Ok((!ranges.is_empty()).then_some(ranges))
}
struct SurfacePcurveContext<'index, 'model> {
model: PassEvaluator<'index, 'model>,
surface_id: &'index crate::ids::SurfaceId,
geometry: &'index SurfaceGeometry,
}
fn edge_pcurve_parameter_ranges(
ctx: &DecodeContext<'_>,
context: &SurfacePcurveContext<'_, '_>,
curve_geometry: Option<&crate::geometry::CurveGeometry>,
endpoints: [Point3; 2],
first: &crate::geometry::pcurve::Pcurve,
last: &crate::geometry::pcurve::Pcurve,
tolerance: f64,
) -> Result<Option<Vec<[f64; 2]>>, CodecError> {
let [start, end] = endpoints;
let mut start_parameters = Vec::new();
for seed in pcurve_parameter_seeds_on_surface(ctx, context, first)? {
if let Some(parameter) = mapped_pcurve_parameter_near_point(
ctx,
context,
&first.geometry,
start,
seed,
tolerance,
)? {
start_parameters.push(parameter);
}
}
let start_parameters = unique(start_parameters, Some);
let mut end_parameters = Vec::new();
for seed in pcurve_parameter_seeds_on_surface(ctx, context, last)? {
if let Some(parameter) =
mapped_pcurve_parameter_near_point(ctx, context, &last.geometry, end, seed, tolerance)?
{
end_parameters.push(parameter);
}
}
let end_parameters = unique(end_parameters, Some);
let ranges = parameter_pairs(&start_parameters, &end_parameters);
if !ranges.is_empty() {
return Ok(Some(ranges));
}
drop(ranges);
drop(start_parameters);
drop(end_parameters);
let Some(curve_geometry) = curve_geometry else {
return Ok(None);
};
if !matches!(
curve_geometry,
crate::geometry::CurveGeometry::Solved(
SolvedCurveGeometry::Circle(_)
| SolvedCurveGeometry::Ellipse(_)
| SolvedCurveGeometry::Parabola(_)
| SolvedCurveGeometry::Hyperbola(_)
)
) {
return Ok(None);
}
let mut seeds = pcurve_parameter_seeds_on_surface(ctx, context, first)?;
seeds.extend(pcurve_parameter_seeds_on_surface(ctx, context, last)?);
let mut start_parameters = Vec::new();
for seed in &seeds {
if let Some(parameter) =
curve_parameter_near_point(ctx, curve_geometry, start, seed.get(), tolerance)?
{
start_parameters.push(parameter);
}
}
let start_parameters = unique(start_parameters, Some);
let mut end_parameters = Vec::new();
for seed in &seeds {
if let Some(parameter) =
curve_parameter_near_point(ctx, curve_geometry, end, seed.get(), tolerance)?
{
end_parameters.push(parameter);
}
}
let end_parameters = unique(end_parameters, Some);
let ranges = parameter_pairs(&start_parameters, &end_parameters);
Ok((!ranges.is_empty()).then_some(ranges))
}
fn parameter_pairs(starts: &[FiniteReal], ends: &[FiniteReal]) -> Vec<[f64; 2]> {
let mut pairs = Vec::new();
for start in starts {
for end in ends {
pairs.push([start.get(), end.get()]);
}
}
pairs
}
fn mapped_pcurve_parameter_near_point(
ctx: &DecodeContext<'_>,
context: &SurfacePcurveContext<'_, '_>,
pcurve_geometry: &PcurveGeometry,
target: Point3,
seed: FiniteReal,
tolerance: f64,
) -> Result<Option<FiniteReal>, ResourceLimit> {
if !tolerance.is_finite() || tolerance < 0.0 {
return Ok(None);
}
let domain = pcurve_parameter_domain(ctx, pcurve_geometry)?.map(ParameterInterval::from);
let stepped = |parameter: FiniteReal, step: FiniteReal| match domain {
Some(domain) => {
Some(domain.project(ExtendedReal::stepped(parameter, FiniteReal::ONE, step)))
}
None => FiniteReal::new(parameter.get() - step.get()),
};
let uv_at = |parameter: FiniteReal| match crate::eval::decode::pcurve_uv(
ctx,
pcurve_geometry,
parameter.get(),
) {
Ok(uv) => Ok(Some(uv.get())),
Err(failure) => failure.non_finite(),
};
let point_at = |parameter: FiniteReal| -> Result<Option<Point3>, ResourceLimit> {
let Some(uv) = uv_at(parameter)? else {
return Ok(None);
};
let evaluated = if matches!(context.geometry, SurfaceGeometry::Solved(_)) {
crate::eval::decode::surface_point(ctx, context.geometry, uv.u, uv.v)
} else {
context
.model
.surface_point(ctx, context.surface_id, uv.u, uv.v)
};
match evaluated {
Ok(point) => Ok(Some(point.get())),
Err(failure) => failure.non_finite(),
}
};
let tangent_at = |parameter: FiniteReal| -> Result<Option<Vector3>, ResourceLimit> {
let Some(uv) = uv_at(parameter)? else {
return Ok(None);
};
let tangent_uv = match pcurve_tangent(ctx, pcurve_geometry, parameter.get()) {
Ok(tangent) => tangent,
Err(EvaluationFailure::ResourceLimit(limit)) => return Err(limit),
Err(
EvaluationFailure::NoValue
| EvaluationFailure::NonFinite(_)
| EvaluationFailure::WorkExhausted,
) => return Ok(None),
};
let evaluated = if matches!(context.geometry, SurfaceGeometry::Solved(_)) {
crate::eval::surface_partials(ctx, context.geometry, uv.u, uv.v)
} else {
context
.model
.surface_partials(ctx, context.surface_id, uv.u, uv.v)
};
let partials = match evaluated {
Ok(partials) => partials,
Err(EvaluationFailure::ResourceLimit(limit)) => return Err(limit),
Err(
EvaluationFailure::NoValue
| EvaluationFailure::NonFinite(_)
| EvaluationFailure::WorkExhausted,
) => return Ok(None),
};
Ok(Some(Vector3::new(
partials.du.x * tangent_uv.u + partials.dv.x * tangent_uv.v,
partials.du.y * tangent_uv.u + partials.dv.y * tangent_uv.v,
partials.du.z * tangent_uv.u + partials.dv.z * tangent_uv.v,
)))
};
let mismatch = |point: Point3| Point3::distance(point, target);
let mut parameter = domain.map_or(seed, |domain| {
domain.project(ExtendedReal::from_finite(seed))
});
for _ in 0..32 {
let Some(point) = point_at(parameter)? else {
return Ok(None);
};
let error = mismatch(point);
if error.is_finite() && error <= tolerance {
return Ok(Some(parameter));
}
let Some(tangent) = tangent_at(parameter)? else {
return Ok(None);
};
let Some(step) = crate::math::solve::projection_step(tangent, point.vector_from(target))
else {
return Ok(None);
};
let Some(mut candidate) = stepped(parameter, step) else {
return Ok(None);
};
let Some(candidate_point) = point_at(candidate)? else {
return Ok(None);
};
let mut candidate_error = mismatch(candidate_point);
for _ in 0..12 {
if candidate_error <= error {
break;
}
candidate = candidate.midpoint(parameter);
let Some(candidate_point) = point_at(candidate)? else {
return Ok(None);
};
candidate_error = mismatch(candidate_point);
}
if candidate == parameter || !candidate_error.is_finite() || candidate_error >= error {
return Ok(None);
}
parameter = candidate;
}
Ok(None)
}
fn unique<T>(
values: impl IntoIterator<Item = T>,
mut project: impl FnMut(T) -> Option<FiniteReal>,
) -> Vec<FiniteReal> {
let mut unique = Vec::new();
for value in values {
let Some(value) = project(value) else {
continue;
};
let mut present = false;
for candidate in &unique {
if *candidate == value {
present = true;
break;
}
}
if !present {
unique.push(value);
}
}
unique
}
fn pcurve_parameter_seeds(
ctx: &DecodeContext<'_>,
pcurve: &crate::geometry::pcurve::Pcurve,
) -> Result<Vec<f64>, CodecError> {
let mut seeds = Vec::new();
seeds.push(0.0);
if let Some(range) = pcurve.parameter_range() {
seeds.extend(range.get());
}
if let Some(domain) = pcurve_parameter_domain(ctx, &pcurve.geometry)? {
let [start, end] = domain.endpoints();
seeds.extend([start, start.midpoint(end), end]);
}
Ok(seeds)
}
fn pcurve_parameter_seeds_on_surface(
ctx: &DecodeContext<'_>,
context: &SurfacePcurveContext<'_, '_>,
pcurve: &crate::geometry::pcurve::Pcurve,
) -> Result<Vec<FiniteReal>, CodecError> {
let mut seeds = pcurve_parameter_seeds(ctx, pcurve)?;
let Some((origin, direction)) = pcurve.geometry.line_parameters(ctx)? else {
return Ok(unique(seeds, FiniteReal::new));
};
let domains = match context.geometry.solved() {
Some(geometry) => solved_surface_parameter_domains(ctx, geometry)?,
None => None,
};
let Some([[u_lower, u_upper], [v_lower, v_upper]]) = domains else {
return Ok(unique(seeds, FiniteReal::new));
};
for boundary in [u_lower, u_lower.midpoint(u_upper), u_upper] {
if direction.u != 0.0 {
seeds.push((boundary - origin.u) / direction.u);
}
}
for boundary in [v_lower, v_lower.midpoint(v_upper), v_upper] {
if direction.v != 0.0 {
seeds.push((boundary - origin.v) / direction.v);
}
}
Ok(unique(seeds, FiniteReal::new))
}
fn solved_surface_parameter_domains(
ctx: &DecodeContext<'_>,
geometry: &SolvedSurfaceGeometry,
) -> Result<Option<[[f64; 2]; 2]>, ResourceLimit> {
let _depth = ctx.enter_nested_limit("surface parameter domain nesting")?;
Ok(match geometry {
SolvedSurfaceGeometry::Nurbs(surface) => {
let u_count = surface.u_count();
let v_count = surface.v_count();
match (
nurbs_pcurve_parameter_domain(surface.u_degree(), surface.u_knots(), u_count),
nurbs_pcurve_parameter_domain(surface.v_degree(), surface.v_knots(), v_count),
) {
(Some(u), Some(v)) => Some([u.endpoints(), v.endpoints()]),
_ => None,
}
}
SolvedSurfaceGeometry::Transformed(placed) => {
solved_surface_parameter_domains(ctx, placed.basis())?
}
SolvedSurfaceGeometry::Plane(_)
| SolvedSurfaceGeometry::Cylinder(_)
| SolvedSurfaceGeometry::Cone(_)
| SolvedSurfaceGeometry::Sphere(_)
| SolvedSurfaceGeometry::Torus(_)
| SolvedSurfaceGeometry::Polygonal(_)
| SolvedSurfaceGeometry::Unknown { .. } => None,
})
}
fn pcurve_parameter_extremes(
ctx: &DecodeContext<'_>,
pcurve: &crate::geometry::pcurve::Pcurve,
) -> Result<Option<[f64; 2]>, ResourceLimit> {
match pcurve
.parameter_range()
.map(crate::units::FiniteVector::get)
{
Some(range) => Ok(Some(range)),
None => pcurve_geometry_trim_range(ctx, &pcurve.geometry),
}
}
fn pcurve_geometry_trim_range(
ctx: &DecodeContext<'_>,
geometry: &PcurveGeometry,
) -> Result<Option<[f64; 2]>, ResourceLimit> {
let _depth = ctx.enter_nested_limit("pcurve trim range nesting")?;
Ok(match geometry {
PcurveGeometry::Trimmed(trimmed_pcurve) => {
let parameter_range = trimmed_pcurve.parameter_range();
Some(parameter_range.endpoints())
}
PcurveGeometry::Offset(offset_pcurve) => {
let basis = offset_pcurve.basis();
pcurve_geometry_trim_range(ctx, basis)?
}
PcurveGeometry::Transformed(placed) => pcurve_geometry_trim_range(ctx, placed.basis())?,
PcurveGeometry::Line(_) => None,
PcurveGeometry::Circle(_) => None,
PcurveGeometry::Ellipse(_) => None,
PcurveGeometry::Harmonic(_) => None,
PcurveGeometry::Parabola(_) => None,
PcurveGeometry::Hyperbola(_) => None,
PcurveGeometry::Hyperbolic(_) => None,
PcurveGeometry::PolarHarmonic(_) => None,
PcurveGeometry::PolarNurbs { .. } => None,
PcurveGeometry::Nurbs { .. } => None,
PcurveGeometry::SphericalGreatCircle(_) => None,
})
}
#[cfg(test)]
mod tests;