#![allow(clippy::unwrap_used)]
use cadmpeg_test_support::edit;
use super::sketch_curve_offset_matches;
use crate::examples::unit_cube;
use crate::math::{Point2, Point3, Vector3};
use crate::report::check::Check;
use crate::sketches::{SketchGeometry, SketchGeometryDefinition};
use crate::validate::validate_neutral;
use crate::CadIr;
use crate::{
features::ExtrudeDirection,
scalar::{Angle, Length},
};
const TEST_LINEAR_TOLERANCE: f64 = 1.0e-6;
#[test]
fn trimmed_concentric_arcs_validate_as_offsets() {
let arc = |radius, start, end| {
SketchGeometry::try_from(SketchGeometryDefinition::Arc {
center: Point2::new(3.0, -4.0),
radius: Length::new(radius).unwrap(),
start_angle: Angle::new(start).unwrap(),
end_angle: Angle::new(end).unwrap(),
})
.unwrap()
};
let source = arc(2.0, 0.0, std::f64::consts::FRAC_PI_2);
let trimmed_result = arc(5.0, 0.1, 1.4);
let disjoint_result = arc(5.0, std::f64::consts::PI, 3.0 * std::f64::consts::FRAC_PI_2);
assert!(sketch_curve_offset_matches(
&source,
&trimmed_result,
-3.0,
TEST_LINEAR_TOLERANCE
));
assert!(!sketch_curve_offset_matches(
&source,
&disjoint_result,
-3.0,
TEST_LINEAR_TOLERANCE
));
}
#[test]
fn full_concentric_circles_validate_as_offsets() {
let circle = |radius| {
SketchGeometry::try_from(SketchGeometryDefinition::Circle {
center: Point2::new(3.0, -4.0),
radius: Length::new(radius).unwrap(),
})
.unwrap()
};
let source = circle(5.0);
let result = circle(3.5);
let displaced = SketchGeometry::try_from(SketchGeometryDefinition::Circle {
center: Point2::new(3.0, -3.9),
radius: Length::new(3.5).unwrap(),
})
.unwrap();
assert!(sketch_curve_offset_matches(
&source,
&result,
1.5,
TEST_LINEAR_TOLERANCE
));
assert!(sketch_curve_offset_matches(
&result,
&source,
-1.5,
TEST_LINEAR_TOLERANCE
));
assert!(!sketch_curve_offset_matches(
&source,
&displaced,
1.5,
TEST_LINEAR_TOLERANCE
));
}
#[test]
fn mixed_full_circle_arc_validate_as_offsets() {
let circle = SketchGeometry::try_from(SketchGeometryDefinition::Circle {
center: Point2::new(3.0, -4.0),
radius: Length::new(5.0).unwrap(),
})
.unwrap();
let arc = SketchGeometry::try_from(SketchGeometryDefinition::Arc {
center: Point2::new(3.0, -4.0),
radius: Length::new(3.5).unwrap(),
start_angle: Angle::new(0.1).unwrap(),
end_angle: Angle::new(1.4).unwrap(),
})
.unwrap();
let displaced = SketchGeometry::try_from(SketchGeometryDefinition::Arc {
center: Point2::new(3.1, -4.0),
radius: Length::new(3.5).unwrap(),
start_angle: Angle::new(0.1).unwrap(),
end_angle: Angle::new(1.4).unwrap(),
})
.unwrap();
assert!(sketch_curve_offset_matches(
&circle,
&arc,
1.5,
TEST_LINEAR_TOLERANCE
));
assert!(sketch_curve_offset_matches(
&arc,
&circle,
-1.5,
TEST_LINEAR_TOLERANCE
));
assert!(!sketch_curve_offset_matches(
&circle,
&displaced,
1.5,
TEST_LINEAR_TOLERANCE
));
}
#[test]
fn fitted_nurbs_offsets_validate_from_clamped_endpoint_frames() {
use crate::math::{Point2, Point3, Vector3};
use crate::scalar::Length;
use crate::sketches::{
Sketch, SketchConstraint, SketchConstraintDefinitionInput, SketchConstraintId,
SketchEntity, SketchEntityId, SketchGeometry, SketchGeometryDefinition, SketchId,
SketchOffsetPair,
};
let mut ir = CadIr::empty();
let sketch = SketchId::mint("synthetic:test:sketch#nurbs-offset").unwrap();
ir.model.sketches.push(Sketch {
id: sketch.clone(),
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
profiles: crate::sketches::SketchProfiles::default(),
native_ref: None,
});
let source = SketchEntityId::mint("synthetic:test:nurbs#source").unwrap();
let result = SketchEntityId::mint("synthetic:test:nurbs#result").unwrap();
let result_start = Point2::new(-1.2, 1.6);
let result_end = Point2::new(10.0 + 2.0 / 5.0_f64.sqrt(), 4.0 / 5.0_f64.sqrt());
ir.model.sketch_entities.extend([
SketchEntity::new(
source.clone(),
sketch.clone(),
SketchGeometry::nurbs(
crate::geometry::pcurve::PcurveNurbs::from_lanes(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point2::new(0.0, 0.0),
Point2::new(4.0, 3.0),
Point2::new(10.0, 0.0),
],
None,
false,
)
.unwrap(),
),
),
SketchEntity::new(
result.clone(),
sketch.clone(),
SketchGeometry::nurbs(
crate::geometry::pcurve::PcurveNurbs::from_lanes(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![
result_start,
Point2::new(result_start.u + 2.0, result_start.v + 1.5),
Point2::new(result_end.u - 3.0, result_end.v + 1.5),
result_end,
],
None,
false,
)
.unwrap(),
),
),
]);
let constraint = SketchConstraintId::mint("synthetic:test:constraint#nurbs-offset").unwrap();
ir.model.sketch_constraints.push(SketchConstraint {
id: constraint.clone(),
sketch,
definition: crate::sketches::SketchConstraintDefinition::try_from(
SketchConstraintDefinitionInput::Offset {
pairs: vec![SketchOffsetPair {
source: source.clone(),
result: result.clone(),
source_reversed: false,
}],
distance: Length::new(2.0).unwrap(),
parameter: None,
},
)
.unwrap(),
name: None,
driving: None,
active: None,
virtual_space: None,
visible: None,
orientation: None,
label_distance: None,
label_position: None,
metadata: None,
native_ref: None,
});
ir.finalize();
let source_ordinal = ir
.model
.sketch_entities
.iter()
.position(|entity| entity.id() == &source)
.expect("source entity");
let result_ordinal = ir
.model
.sketch_entities
.iter()
.position(|entity| entity.id() == &result)
.expect("result entity");
let offset_mismatch = |report: &crate::report::check::ValidationReport| {
report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(constraint.as_str())
&& finding
.message
.contains("offset pair does not match its oriented distance")
})
};
assert!(!offset_mismatch(
&validate_neutral(&ir, Vec::new()).expect("validate the neutral model")
));
edit::replace(
&mut ir.model.sketch_entities[result_ordinal].geometry,
|previous| {
let mut definition = previous.definition().to_raw();
{
let definition: &mut crate::sketches::SketchGeometryDefinition = &mut definition;
let SketchGeometryDefinition::Nurbs { curve } = definition else {
unreachable!("test result is a NURBS")
};
curve.reverse_parameterization();
};
definition.try_into()
},
)
.unwrap();
let reversed_distance = crate::eval::fitted_nurbs_offset_frame_distance(
&ir.model.sketch_entities[source_ordinal].geometry,
&ir.model.sketch_entities[result_ordinal].geometry,
ir.tolerances.linear.get(),
)
.expect("reversed fitted offset frame")
.get();
assert!(
(reversed_distance - 2.0).abs() <= 1.0e-9,
"reversed fitted offset distance {reversed_distance}"
);
assert!(!offset_mismatch(
&validate_neutral(&ir, Vec::new()).expect("validate the neutral model")
));
edit::replace(
&mut ir.model.sketch_entities[result_ordinal].geometry,
|previous| {
let mut definition = previous.definition().to_raw();
{
let definition: &mut crate::sketches::SketchGeometryDefinition = &mut definition;
let SketchGeometryDefinition::Nurbs { curve } = definition else {
unreachable!("test result is a NURBS")
};
curve.reverse_parameterization();
let last = curve.pole_rows().count() - 1;
curve
.try_map_control_points(|pole_index, point| {
let point = point.get();
crate::units::FinitePoint2::new(crate::math::Point2::new(
point.u + if pole_index == last { 0.01 } else { 0.0 },
point.v,
))
.ok_or(())
})
.unwrap();
};
definition.try_into()
},
)
.unwrap();
assert!(offset_mismatch(
&validate_neutral(&ir, Vec::new()).expect("validate the neutral model")
));
}
#[test]
fn sketch_profiles_and_constraints_enforce_local_connectivity() {
use crate::math::{Point2, Point3, Vector3};
use crate::sketches::{
Sketch, SketchConstraint, SketchConstraintDefinitionInput, SketchConstraintId,
SketchEntity, SketchEntityId, SketchEntityUse, SketchGeometry, SketchGeometryDefinition,
SketchId,
};
let mut ir = unit_cube().expect("valid unit cube fixture");
let first_sketch = SketchId::mint("synthetic:test:sketch#first").unwrap();
let second_sketch = SketchId::mint("synthetic:test:sketch#second").unwrap();
let first = SketchEntityId::mint("synthetic:test:entity#first").unwrap();
let disconnected = SketchEntityId::mint("synthetic:test:entity#disconnected").unwrap();
let foreign = SketchEntityId::mint("synthetic:test:entity#foreign").unwrap();
let plane = |id: SketchId, profiles| Sketch {
id,
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
profiles: crate::sketches::SketchProfiles::try_from(profiles).unwrap(),
native_ref: None,
};
ir.model.sketches.extend([
plane(
first_sketch.clone(),
vec![vec![
SketchEntityUse {
entity: first.clone(),
reversed: false,
},
SketchEntityUse {
entity: disconnected.clone(),
reversed: false,
},
]],
),
plane(second_sketch.clone(), Vec::new()),
]);
let line = |id, sketch, start, end| {
SketchEntity::new(
id,
sketch,
SketchGeometry::try_from(SketchGeometryDefinition::Line { start, end }).unwrap(),
)
};
ir.model.sketch_entities.extend([
line(
first.clone(),
first_sketch.clone(),
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
),
line(
disconnected.clone(),
first_sketch.clone(),
Point2::new(2.0, 0.0),
Point2::new(3.0, 0.0),
),
line(
foreign.clone(),
second_sketch,
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
),
]);
let constraint = SketchConstraintId::mint("synthetic:test:constraint#foreign").unwrap();
ir.model.sketch_constraints.push(SketchConstraint {
id: constraint.clone(),
sketch: first_sketch.clone(),
definition: crate::sketches::SketchConstraintDefinition::try_from(
SketchConstraintDefinitionInput::Parallel {
first,
second: foreign,
},
)
.unwrap(),
name: None,
driving: None,
active: None,
virtual_space: None,
visible: None,
orientation: None,
label_distance: None,
label_position: None,
metadata: None,
native_ref: None,
});
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(first_sketch.as_str())
&& finding.message.contains("disconnected consecutive")
}));
assert!(report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(constraint.as_str())
&& finding.message.contains("different sketch")
}));
let disconnected_geometry = &mut ir
.model
.sketch_entities
.iter_mut()
.find(|entity| entity.id() == &disconnected)
.expect("disconnected entity remains present")
.geometry;
edit::replace(disconnected_geometry, |previous| {
let mut definition = previous.definition().to_raw();
{
let definition: &mut crate::sketches::SketchGeometryDefinition = &mut definition;
let SketchGeometryDefinition::Line { start, .. } = definition else {
unreachable!("second entity is a line")
};
*start = Point2::new(1.0 + ir.tolerances.linear.get() * 0.5, 0.0);
};
definition.try_into()
})
.unwrap();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(!report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(first_sketch.as_str())
&& finding.message.contains("disconnected consecutive")
}));
}
#[test]
fn midpoint_and_fixed_angle_constraints_refuse_an_entity_of_another_kind() {
use crate::scalar::PositiveAngle;
use crate::sketches::{
Sketch, SketchConstraint, SketchConstraintDefinition, SketchConstraintDefinitionInput,
SketchConstraintId, SketchEntity, SketchEntityId, SketchId, SketchLocus,
};
let mut ir = unit_cube().expect("valid unit cube fixture");
let sketch = SketchId::mint("synthetic:test:sketch#kinds").unwrap();
ir.model.sketches.push(Sketch {
id: sketch.clone(),
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
profiles: crate::sketches::SketchProfiles::try_from(Vec::new()).unwrap(),
native_ref: None,
});
let entity =
|name: &str| SketchEntityId::mint(format!("synthetic:test:entity#{name}")).unwrap();
let ellipse = |bounds| SketchGeometryDefinition::Ellipse {
center: Point2::new(0.0, 0.0),
major_angle: Angle::ZERO,
radii: crate::sketches::EllipseRadii {
major_radius: Length::new(2.0).unwrap(),
minor_radius: Length::new(1.0).unwrap(),
},
bounds,
};
for (name, definition) in [
(
"point",
SketchGeometryDefinition::Point {
position: Point2::new(0.0, 0.0),
},
),
(
"line",
SketchGeometryDefinition::Line {
start: Point2::new(-1.0, 0.0),
end: Point2::new(1.0, 0.0),
},
),
(
"circle",
SketchGeometryDefinition::Circle {
center: Point2::new(0.0, 0.0),
radius: Length::new(1.0).unwrap(),
},
),
(
"arc",
SketchGeometryDefinition::Arc {
center: Point2::new(0.0, 0.0),
radius: Length::new(1.0).unwrap(),
start_angle: Angle::ZERO,
end_angle: Angle::new(std::f64::consts::FRAC_PI_2).unwrap(),
},
),
("full-ellipse", ellipse(None)),
(
"bounded-ellipse",
ellipse(Some([
Angle::ZERO,
Angle::new(std::f64::consts::FRAC_PI_2).unwrap(),
])),
),
] {
ir.model.sketch_entities.push(SketchEntity::new(
entity(name),
sketch.clone(),
SketchGeometry::try_from(definition).unwrap(),
));
}
ir.model.sketch_entities.push(SketchEntity::new(
entity("native"),
sketch.clone(),
SketchGeometry::native(cadmpeg_core::text::NonBlankString::new("test").unwrap()),
));
let quarter = PositiveAngle::QUARTER_TURN;
let midpoint = |name: &str| SketchConstraintDefinitionInput::Midpoint {
point: SketchLocus::Entity(entity("point")),
entity: entity(name),
};
let arc_angle = |name: &str| SketchConstraintDefinitionInput::ArcAngle {
entity: entity(name),
angle: quarter,
};
let ellipse_angle = |name: &str| SketchConstraintDefinitionInput::EllipseAngle {
entity: entity(name),
angle: quarter,
};
let midpoint_refusal =
"sketch midpoint constraint references an entity that is not a bounded curve";
let arc_refusal = "sketch arc-angle constraint references an entity that is not a circular arc";
let ellipse_refusal =
"sketch ellipse-angle constraint references an entity that is not a bounded ellipse";
let cases = [
(
"midpoint-circle",
midpoint("circle"),
Some(midpoint_refusal),
),
(
"midpoint-full-ellipse",
midpoint("full-ellipse"),
Some(midpoint_refusal),
),
("midpoint-point", midpoint("point"), Some(midpoint_refusal)),
("midpoint-line", midpoint("line"), None),
("midpoint-arc", midpoint("arc"), None),
(
"midpoint-bounded-ellipse",
midpoint("bounded-ellipse"),
None,
),
("midpoint-native", midpoint("native"), None),
("arc-angle-line", arc_angle("line"), Some(arc_refusal)),
("arc-angle-circle", arc_angle("circle"), Some(arc_refusal)),
("arc-angle-arc", arc_angle("arc"), None),
("arc-angle-native", arc_angle("native"), None),
(
"ellipse-angle-full",
ellipse_angle("full-ellipse"),
Some(ellipse_refusal),
),
(
"ellipse-angle-arc",
ellipse_angle("arc"),
Some(ellipse_refusal),
),
(
"ellipse-angle-bounded",
ellipse_angle("bounded-ellipse"),
None,
),
("ellipse-angle-native", ellipse_angle("native"), None),
];
for (name, definition, _) in &cases {
ir.model.sketch_constraints.push(SketchConstraint {
id: SketchConstraintId::mint(format!("synthetic:test:constraint#{name}")).unwrap(),
sketch: sketch.clone(),
definition: SketchConstraintDefinition::try_from(definition.clone()).unwrap(),
name: None,
driving: None,
active: None,
virtual_space: None,
visible: None,
orientation: None,
label_distance: None,
label_position: None,
metadata: None,
native_ref: None,
});
}
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
for (name, _, refusal) in cases {
let id = format!("synthetic:test:constraint#{name}");
let kind_findings = report
.findings
.iter()
.filter(|finding| {
finding.entity.as_deref() == Some(id.as_str())
&& [midpoint_refusal, arc_refusal, ellipse_refusal]
.contains(&finding.message.as_str())
})
.collect::<Vec<_>>();
match refusal {
Some(message) => {
assert_eq!(kind_findings.len(), 1, "{name}: {kind_findings:?}");
assert_eq!(
kind_findings[0].check,
Check::ReferentialIntegrity,
"{name}"
);
assert_eq!(kind_findings[0].message, message, "{name}");
}
None => assert!(kind_findings.is_empty(), "{name}: {kind_findings:?}"),
}
}
}
#[test]
fn sketch_constraint_native_ref_must_resolve() {
let mut ir = unit_cube().expect("valid unit cube fixture");
let id =
crate::sketches::SketchConstraintId::mint("synthetic:test:sketch-constraint#native-ref")
.unwrap();
ir.model
.sketch_constraints
.push(crate::sketches::SketchConstraint {
id: id.clone(),
sketch: crate::sketches::SketchId::mint("synthetic:test:sketch#missing").unwrap(),
definition: crate::sketches::SketchConstraintDefinition::try_from(
crate::sketches::SketchConstraintDefinitionInput::Native {
native_kind: cadmpeg_core::text::NonBlankString::new("test").unwrap(),
native_state: None,
native_flags: Some(0x4000),
native_properties: std::collections::BTreeMap::from([(
"mode".to_string(),
"7".to_string(),
)]),
entities: Vec::new(),
parameter: None,
operands: vec![crate::sketches::SketchNativeOperand {
native_kind: cadmpeg_core::text::NonBlankString::new("test")
.expect("source operand kind is nonempty"),
field: None,
object_index: Some(0),
native_ref: Some("native:missing-operand#0".into()),
}],
},
)
.unwrap(),
name: None,
driving: None,
active: None,
virtual_space: None,
visible: None,
orientation: None,
label_distance: None,
label_position: None,
metadata: None,
native_ref: Some("native:missing-relation#0".into()),
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| {
finding.check == Check::NativeLinks
&& finding.entity.as_deref() == Some(id.as_str())
&& finding.message.contains("native:missing-relation#0")
}));
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| {
finding.check == Check::NativeLinks
&& finding.entity.as_deref() == Some(id.as_str())
&& finding.message.contains("native:missing-operand#0")
}));
let serialized = serde_json::to_string(&ir).unwrap();
let round_trip = CadIr::from_json(&serialized).unwrap();
assert!(matches!(
round_trip.model.sketch_constraints[0].definition.kind(),
crate::sketches::SketchConstraintDefinitionInput::Native {
native_flags: Some(0x4000),
..
}
));
let crate::sketches::SketchConstraintDefinitionInput::Native {
native_properties, ..
} = round_trip.model.sketch_constraints[0].definition.kind()
else {
unreachable!("test constraint is native")
};
assert_eq!(native_properties.get("mode").map(String::as_str), Some("7"));
let mut legacy = serde_json::from_str::<serde_json::Value>(&serialized).unwrap();
legacy["model"]["sketch_constraints"][0]["definition"]
.as_object_mut()
.unwrap()
.remove("native_properties");
let legacy = CadIr::from_json(&serde_json::to_string(&legacy).unwrap()).unwrap();
let crate::sketches::SketchConstraintDefinitionInput::Native {
native_properties, ..
} = legacy.model.sketch_constraints[0].definition.kind()
else {
unreachable!("test constraint is native")
};
assert!(native_properties.is_empty());
}
#[test]
fn sketch_feature_ownership_and_order_are_validated() {
use crate::features::{
BooleanOp, ExtrudeExtent, ExtrudeSide, Feature, FeatureDefinition, FeatureId,
FeatureOperation, LinearTermination, PlanarProfileRef, ProfileRef,
};
use crate::sketches::{Sketch, SketchId};
let mut ir = unit_cube().expect("valid unit cube fixture");
let sketch_id = SketchId::mint("synthetic:test:sketch#ordered").unwrap();
ir.model.sketches.push(Sketch {
id: sketch_id.clone(),
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
profiles: crate::sketches::SketchProfiles::default(),
native_ref: None,
});
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#consumer").expect("identity grammar"),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile: ProfileRef::Planar(PlanarProfileRef::Sketch(sketch_id.clone())),
direction: ExtrudeDirection::ProfileNormal {},
start: crate::features::ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::Blind {
length: crate::scalar::NonZeroLength::new(1.0).unwrap(),
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: None,
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
),
native_ref: None,
});
for (ordinal, suffix) in [(1, "owner"), (2, "duplicate-owner")] {
ir.model.features.push(Feature {
id: FeatureId::mint(format!("synthetic:test:feature#{suffix}"))
.expect("identity grammar"),
ordinal,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Sketch {
sketch: crate::features::SketchFeatureBinding::Planar(Some(sketch_id.clone())),
}),
),
native_ref: None,
});
}
let findings = validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings;
assert!(findings.iter().any(|finding| finding
.message
.contains("does not precede its profile consumer")));
assert!(findings
.iter()
.any(|finding| finding.message.contains("has multiple owning features")));
}
#[test]
fn sketch_profile_subselections_are_bounds_checked() {
use crate::features::{
BooleanOp, ExtrudeExtent, ExtrudeSide, Feature, FeatureDefinition, FeatureId,
FeatureOperation, LinearTermination, PlanarProfileRef, ProfileRef, SketchProfileRegion,
};
use crate::sketches::{Sketch, SketchEntityId, SketchId};
let mut ir = unit_cube().expect("valid unit cube fixture");
let sketch_id = SketchId::mint("synthetic:test:sketch#selection").unwrap();
ir.model.sketches.push(Sketch {
id: sketch_id.clone(),
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
profiles: crate::sketches::SketchProfiles::default(),
native_ref: None,
});
let feature = |suffix: &str, ordinal, profile| Feature {
id: FeatureId::mint(format!("synthetic:test:feature#{suffix}")).expect("identity grammar"),
ordinal,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile,
direction: ExtrudeDirection::ProfileNormal {},
start: crate::features::ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::Blind {
length: crate::scalar::NonZeroLength::new(1.0).unwrap(),
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: None,
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
),
native_ref: None,
};
ir.model.features.push(feature(
"invalid-profile-index",
1,
ProfileRef::Planar(PlanarProfileRef::sketch_profiles(sketch_id.clone(), vec![0]).unwrap()),
));
ir.model.features.push(feature(
"invalid-region",
2,
ProfileRef::Planar(
PlanarProfileRef::sketch_regions(
sketch_id.clone(),
vec![SketchProfileRegion::loops(0, Vec::new()).unwrap()],
)
.unwrap(),
),
));
let selected_entity = SketchEntityId::mint("synthetic:test:entity#missing").unwrap();
ir.model.features.push(feature(
"repeated-profile-entity",
3,
ProfileRef::Planar(
PlanarProfileRef::sketch_entities(sketch_id.clone(), vec![selected_entity]).unwrap(),
),
));
let findings = validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings;
assert!(findings.iter().any(|finding| {
finding.message == "sketch profile indices are empty, repeated, or out of range"
}));
assert!(findings.iter().any(|finding| {
finding.message
== "sketch regions have empty, repeated, invalid, or out-of-range boundaries"
}));
assert!(findings.iter().any(|finding| {
finding.message
== "sketch profile entities are empty, repeated, missing, or owned by another sketch"
}));
}
#[test]
fn spatial_sketch_feature_owns_spatial_geometry() {
use crate::features::{Feature, FeatureDefinition, FeatureId, FeatureOperation};
use crate::sketches::{SpatialSketch, SpatialSketchId};
let mut ir = unit_cube().expect("valid unit cube fixture");
let sketch_id = SpatialSketchId::mint("synthetic:test:spatial-sketch#owned").unwrap();
ir.model.spatial_sketches.push(SpatialSketch {
id: sketch_id.clone(),
name: None,
configuration: None,
visible: None,
profiles: Vec::new(),
native_ref: None,
});
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#spatial-sketch").expect("identity grammar"),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::SpatialSketch {
sketch: Some(sketch_id),
}),
),
native_ref: None,
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.is_empty());
let mut duplicate = ir.model.features.last().expect("spatial owner").clone();
duplicate.id = FeatureId::mint("synthetic:test:feature#duplicate-spatial-sketch")
.expect("identity grammar");
duplicate.ordinal = 1;
ir.model.features.push(duplicate);
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message.contains("has multiple owning features")));
}
#[test]
fn spatial_distance_validation_rejects_overflowed_squared_norms() {
use crate::features::{DesignParameter, ParameterId, ParameterValue};
use crate::sketches::{
SketchConstraintId, SpatialSketch, SpatialSketchConstraint,
SpatialSketchConstraintDefinition, SpatialSketchConstraintDefinitionInput,
SpatialSketchEntity, SpatialSketchEntityId, SpatialSketchGeometry,
SpatialSketchGeometryDefinition, SpatialSketchId,
};
let mut ir = CadIr::empty();
let sketch = SpatialSketchId::mint("test:model:sketch#distance").unwrap();
ir.model.spatial_sketches.push(SpatialSketch {
id: sketch.clone(),
name: None,
configuration: None,
visible: None,
profiles: vec![],
native_ref: None,
});
let first = SpatialSketchEntityId::mint("test:model:entity#first").unwrap();
let second = SpatialSketchEntityId::mint("test:model:entity#second").unwrap();
for (id, x) in [(first.clone(), 0.0), (second.clone(), 1e200)] {
ir.model
.spatial_sketch_entities
.push(SpatialSketchEntity::new(
id,
sketch.clone(),
SpatialSketchGeometry::try_from(SpatialSketchGeometryDefinition::Point {
position: Point3::new(x, 0.0, 0.0),
})
.unwrap(),
));
}
let parameter = ParameterId::mint("test:model:parameter#distance").unwrap();
ir.model.parameters.push(DesignParameter {
id: parameter.clone(),
owner: None,
ordinal: 0,
name: "distance".into(),
expression: "1 mm".into(),
display: None,
value: Some(ParameterValue::Length(Length::new(1.0).unwrap())),
dependencies: crate::features::DistinctMembers::default(),
properties: std::collections::BTreeMap::default(),
pmi: None,
native_ref: None,
});
ir.model
.spatial_sketch_constraints
.push(SpatialSketchConstraint {
id: SketchConstraintId::mint("test:model:constraint#distance").unwrap(),
sketch,
definition: SpatialSketchConstraintDefinition::try_from(
SpatialSketchConstraintDefinitionInput::PointDistance {
first,
second,
parameter,
},
)
.unwrap(),
native_ref: None,
});
let report = validate_neutral(&ir, vec![]).expect("validate the neutral model");
assert!(report.findings.iter().any(|finding| finding
.message
.contains("spatial point distance requires two points")));
ir.model.parameters[0].value = Some(ParameterValue::Length(Length::new(1e200).unwrap()));
let report = validate_neutral(&ir, vec![]).expect("validate the neutral model");
assert!(!report.findings.iter().any(|finding| finding
.message
.contains("spatial point distance requires two points")));
}
#[test]
fn extreme_lines_preserve_parallelism_and_span_separation() {
let line = |start, end| {
SketchGeometry::try_from(SketchGeometryDefinition::Line { start, end }).unwrap()
};
let horizontal = line(Point2::new(0.0, 0.0), Point2::new(1e200, 0.0));
let perpendicular = line(Point2::new(0.0, 1.0), Point2::new(0.0, 1e200));
assert!(super::planar_parallel_line_distance(&horizontal, &perpendicular).is_none());
let separated = line(Point2::new(2e200, 1.0), Point2::new(3e200, 1.0));
assert_eq!(
super::planar_parallel_line_distance(&horizontal, &separated)
.map(crate::scalar::FiniteReal::get),
Some(1.0)
);
assert!(super::planar_parallel_line_span_distance(
&horizontal,
&separated,
TEST_LINEAR_TOLERANCE
)
.is_none());
let overlapping = line(Point2::new(0.5e200, 1.0), Point2::new(1.5e200, 1.0));
assert_eq!(
super::planar_parallel_line_span_distance(&horizontal, &overlapping, TEST_LINEAR_TOLERANCE)
.map(crate::scalar::FiniteReal::get),
Some(1.0)
);
}
mod admission;
#[test]
fn spatial_sketch_endpoint_helper_preserves_session_depth_refusal() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let curve = crate::geometry::nurbs::NurbsCurve::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
None,
false,
)
.expect("valid curve");
let geometry = crate::sketches::SpatialSketchGeometry::try_from(
crate::sketches::SpatialSketchGeometryDefinition::Nurbs {
curve: curve.try_into().expect("valid spatial NURBS"),
},
)
.expect("valid spatial geometry");
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("empty root");
assert_eq!(
super::spatial_oriented_endpoints(&ctx, &geometry, false),
Ok(Some((
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0)
)))
);
let original = ctx
.enter_nested_limit("spatial endpoint test outer frame")
.expect_err("outer frame refuses");
let limit = super::spatial_oriented_endpoints(&ctx, &geometry, false)
.expect_err("first evaluator frame refuses");
assert_eq!(limit, original);
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!((limit.limit, limit.used, limit.additional), (0, 0, 1));
assert_eq!(ctx.resource_refusal(), Some(limit));
assert_eq!(
super::spatial_oriented_endpoints(&ctx, &geometry, true),
Err(limit)
);
}