#![allow(clippy::unwrap_used)]
use crate::features::TrimCellSelection;
use crate::math::{Point3, Vector3};
mod distinct_members;
mod revolve;
mod unit_directions;
#[test]
fn native_feature_kind_preserves_the_source_spelling() {
use crate::features::NativeFeatureKind;
for (wire, expected) in [
("\"Canvas\"", NativeFeatureKind::Canvas),
(
"\"PartDesign::FeatureBase\"",
NativeFeatureKind::Other("PartDesign::FeatureBase".into()),
),
] {
let kind: NativeFeatureKind = serde_json::from_str(wire).unwrap();
assert_eq!(kind, expected);
assert_eq!(serde_json::to_string(&kind).unwrap(), wire);
}
}
#[test]
fn a_face_maker_is_its_class_and_carries_no_mode_key() {
use crate::features::FaceMaker;
#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
struct ExtrusionCarrier {
#[serde(default)]
maker: Option<FaceMaker>,
}
assert_eq!(
serde_json::from_value::<FaceMaker>(serde_json::json!("Part::FaceMakerUnified")).unwrap(),
FaceMaker::Unified,
);
assert_eq!(
serde_json::to_value(FaceMaker::Bullseye).unwrap(),
serde_json::json!("Part::FaceMakerBullseye"),
);
assert!(serde_json::from_value::<FaceMaker>(serde_json::json!("")).is_err());
let wire = serde_json::json!({ "maker": "Part::FaceMakerBullseye" });
let carrier = serde_json::from_value::<ExtrusionCarrier>(wire.clone()).unwrap();
assert_eq!(
carrier,
ExtrusionCarrier {
maker: Some(FaceMaker::Bullseye),
}
);
assert_eq!(serde_json::to_value(carrier).unwrap(), wire);
let error = serde_json::from_value::<ExtrusionCarrier>(serde_json::json!({
"maker": { "class": "Part::FaceMakerBullseye", "mode": 3 }
}))
.unwrap_err()
.to_string();
assert!(error.contains("invalid type: map"), "{error}");
}
#[test]
fn draft_anchor_round_trips_as_a_nested_key() {
use crate::features::{DraftAnchor, FeatureDefinition, FeatureOperation};
let wire = serde_json::json!({
"definition": "draft",
"faces": {"kind": "native", "value": "draft:faces"},
"anchor": {
"kind": "parting_line",
"tool": {"kind": "native", "value": "draft:parting-tool"},
"pull": {
"direction": {"x": 0.0, "y": 0.0, "z": 1.0},
"plane": "test:draft:plane#pull"
}
},
"angle": 0.1,
"outward": false
});
let definition: FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(
&definition,
FeatureDefinition::Operation(FeatureOperation::Draft {
anchor: DraftAnchor::PartingLine { .. },
..
})
));
assert_eq!(serde_json::to_value(definition).unwrap(), wire);
}
#[test]
fn wrap_mode_round_trips_as_a_nested_key() {
use crate::features::{FeatureDefinition, FeatureOperation, WrapMode};
let wire = serde_json::json!({
"definition": "wrap",
"profile": {"kind": "native", "value": "wrap:profile"},
"face": {"kind": "native", "value": "wrap:face"},
"mode": {"emboss": {"depth": 2.5}}
});
let definition: FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(
&definition,
FeatureDefinition::Operation(FeatureOperation::Wrap {
mode: WrapMode::Emboss { depth: actual_depth },
..
}) if actual_depth.get() == 2.5
));
assert_eq!(serde_json::to_value(definition).unwrap(), wire);
let scribe = FeatureDefinition::Operation(FeatureOperation::Wrap {
profile: crate::features::PlanarProfileRef::Native("wrap:profile".into()),
face: crate::features::FaceSelection::Native("wrap:face".into()),
mode: WrapMode::Scribe,
});
let encoded = serde_json::to_value(scribe).unwrap();
assert_eq!(encoded.get("mode"), Some(&serde_json::json!("scribe")));
}
#[test]
fn helix_shape_round_trips_as_a_nested_key() {
use crate::features::{FeatureDefinition, FeatureOperation, HelixShape};
let conical_wire = serde_json::json!({
"definition": "helix",
"axis_origin": {"x": 0.0, "y": 0.0, "z": 0.0},
"axis_direction": {"x": 0.0, "y": 0.0, "z": 1.0},
"radius": 2.0,
"shape": {"kind": "conical", "pitch": 3.0, "cone_angle": 0.2},
"revolutions": 4.0,
"start_angle": 0.0,
"clockwise": false
});
let conical: FeatureDefinition = serde_json::from_value(conical_wire.clone()).unwrap();
assert!(matches!(
&conical,
FeatureDefinition::Operation(FeatureOperation::Helix {
shape: HelixShape::Conical { pitch, .. },
..
}) if pitch.get() == 3.0
));
assert_eq!(serde_json::to_value(conical).unwrap(), conical_wire);
let spiral_wire = serde_json::json!({
"definition": "helix",
"axis_origin": {"x": 0.0, "y": 0.0, "z": 0.0},
"axis_direction": {"x": 0.0, "y": 0.0, "z": 1.0},
"radius": 2.0,
"shape": {"kind": "spiral", "radial_growth": 1.5},
"revolutions": 4.0,
"start_angle": 0.0,
"clockwise": false
});
let spiral: FeatureDefinition = serde_json::from_value(spiral_wire.clone()).unwrap();
assert!(matches!(
&spiral,
FeatureDefinition::Operation(FeatureOperation::Helix {
shape: HelixShape::Spiral {
radial_growth: actual_radial_growth
},
..
}) if actual_radial_growth.get() == 1.5
));
assert_eq!(serde_json::to_value(spiral).unwrap(), spiral_wire);
}
#[test]
fn trim_cell_selection_requires_unique_in_range_ordinals() {
let valid = TrimCellSelection::new(vec![1, 4], 5).unwrap();
assert_eq!(valid.removed, &[1, 4]);
assert_eq!(valid.total, 5);
assert!(TrimCellSelection::new(vec![1, 1], 5).is_none());
assert!(TrimCellSelection::new(vec![6], 5).is_none());
}
#[test]
fn trim_cells_preserve_the_nested_wire_fields_and_reject_invalid_input() {
use crate::features::{
FaceSelection, FeatureDefinition, FeatureOperation, PathRef, TrimRegion,
};
let definition = FeatureDefinition::Operation(FeatureOperation::TrimSurface {
faces: FaceSelection::Unresolved,
tool: PathRef::Unresolved("test:trim-tool".into()),
keep: TrimRegion::Cells(TrimCellSelection::new(vec![1, 4], 5).unwrap()),
});
let wire = serde_json::to_value(&definition).unwrap();
assert_eq!(wire["definition"], "trim_surface");
assert_eq!(wire["keep"]["cells"]["removed"], serde_json::json!([1, 4]));
assert_eq!(wire["keep"]["cells"]["total"], 5);
let decoded: FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(
decoded,
FeatureDefinition::Operation(FeatureOperation::TrimSurface {
keep: TrimRegion::Cells(ref selection),
..
}) if selection.removed == [1, 4] && selection.total == 5
));
let mut invalid = wire;
invalid["keep"]["cells"]["removed"] = serde_json::json!([6]);
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
}
#[test]
fn revolve_construction_carries_its_axis_reference_inside_the_axis() {
use crate::features::{FeatureDefinition, FeatureOperation, PathRef, RevolveConstruction};
let wire = serde_json::json!({
"definition": "revolve",
"construction": {
"state": "resolved",
"profile": {"kind": "sketch", "value": "test:model:sketch#profile"},
"axis": {
"origin": {"x": 0.0, "y": 0.0, "z": 0.0},
"direction": {"x": 0.0, "y": 0.0, "z": 1.0},
"reference": {"kind": "native", "value": "test:axis"}
},
"extent": {
"kind": "one_sided",
"termination": {"kind": "angle", "angle": 1.25}
},
"solid": true,
"face_maker_class": "Part::FaceMakerBullseye"
},
"op": "new_body"
});
let definition: FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(
definition,
FeatureDefinition::Operation(FeatureOperation::Revolve {
construction: RevolveConstruction::Resolved { ref axis, .. },
..
}) if axis.reference == Some(PathRef::Native("test:axis".into()))
));
assert_eq!(serde_json::to_value(definition).unwrap(), wire);
let mut sibling = wire;
sibling["construction"].as_object_mut().unwrap().insert(
"axis_reference".to_string(),
serde_json::json!({"kind": "native", "value": "test:axis"}),
);
let error = serde_json::from_value::<FeatureOperation>(sibling)
.unwrap_err()
.to_string();
assert!(error.contains("axis_reference"), "{error}");
}
#[test]
fn revolve_construction_admits_only_typed_partial_states() {
use crate::features::{FeatureDefinition, FeatureOperation, RevolveConstruction};
let partial_wire = serde_json::json!({
"definition": "revolve",
"construction": {
"state": "unresolved",
"missing": "axis",
"profile": {"kind": "native", "value": "test:profile"},
"solid": false
},
"op": "unresolved"
});
let partial: FeatureDefinition = serde_json::from_value(partial_wire.clone()).unwrap();
assert!(matches!(
partial,
FeatureDefinition::Operation(FeatureOperation::Revolve {
construction: RevolveConstruction::Unresolved(_),
..
})
));
assert_eq!(serde_json::to_value(partial).unwrap(), partial_wire);
let resolved_without_a_profile = serde_json::json!({
"definition": "revolve",
"construction": {
"state": "resolved",
"axis": {
"origin": {"x": 0.0, "y": 0.0, "z": 0.0},
"direction": {"x": 0.0, "y": 0.0, "z": 1.0}
},
"extent": {
"kind": "one_sided",
"termination": {"kind": "angle", "angle": 1.25}
}
},
"op": "unresolved"
});
let error = serde_json::from_value::<FeatureOperation>(resolved_without_a_profile)
.unwrap_err()
.to_string();
assert!(error.contains("profile"), "{error}");
}
#[test]
fn an_unresolved_revolve_names_its_first_missing_operand() {
use crate::features::{PartialRevolveConstruction, RevolveConstruction};
let axis = serde_json::json!({
"origin": {"x": 0.0, "y": 0.0, "z": 0.0},
"direction": {"x": 0.0, "y": 0.0, "z": 1.0}
});
let extent = serde_json::json!({
"kind": "one_sided",
"termination": {"kind": "angle", "angle": 1.25}
});
let profile = serde_json::json!({"kind": "native", "value": "test:profile"});
let contradicting = serde_json::json!({
"state": "unresolved",
"missing": "axis",
"profile": profile,
"axis": axis,
});
let error = serde_json::from_value::<RevolveConstruction>(contradicting)
.unwrap_err()
.to_string();
assert!(error.contains("axis"), "{error}");
let untyped = serde_json::json!({
"state": "unresolved",
"profile": profile,
"axis": axis,
"extent": extent,
});
let error = serde_json::from_value::<RevolveConstruction>(untyped)
.unwrap_err()
.to_string();
assert!(error.contains("missing"), "{error}");
let named = serde_json::json!({
"state": "unresolved",
"missing": "profile",
"axis": axis,
});
let construction: RevolveConstruction =
serde_json::from_value(named.clone()).expect("reads the named partial state");
assert!(matches!(
construction,
RevolveConstruction::Unresolved(PartialRevolveConstruction::Profile {
axis: Some(_),
extent: None,
..
})
));
assert_eq!(serde_json::to_value(&construction).unwrap(), named);
}
#[test]
fn extrude_direction_round_trips_as_a_nested_key() {
use crate::features::{
ExtrudeDirection, ExtrusionDirectionSource, FeatureDefinition, FeatureOperation, PathRef,
};
let wire = serde_json::json!({
"definition": "extrude",
"profile": {"kind": "native", "value": "test:profile"},
"direction": {
"kind": "explicit",
"vector": {"x": 0.0, "y": 1.0, "z": 0.0},
"source": {
"kind": "edge",
"reference": {"kind": "native", "value": "test:direction-edge"}
}
},
"start": {"kind": "profile_plane"},
"extent": {
"kind": "one_sided",
"side": {
"termination": {"kind": "blind", "length": 4.0}
}
},
"op": "new_body",
"solid": true
});
let definition: FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert!(matches!(
definition,
FeatureDefinition::Operation(FeatureOperation::Extrude {
direction: ExtrudeDirection::Explicit {
source: Some(ExtrusionDirectionSource::Edge {
reference: PathRef::Native(ref reference),
}),
..
},
..
}) if reference == "test:direction-edge"
));
assert_eq!(serde_json::to_value(definition).unwrap(), wire);
}
#[test]
fn per_edge_flange_widths_reject_empty_rosters_and_preserve_array_wire() {
let wire = serde_json::json!({
"kind": "two_sides_per_edge",
"value": {"widths": [{"first": 3.0, "second": 1.5}, {"first": 2.0, "second": 4.0}]}
});
let width: super::SheetMetalFlangeWidth = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(width).unwrap(), wire);
let mut empty = wire;
empty["value"]["widths"] = serde_json::json!([]);
let error = serde_json::from_value::<super::SheetMetalFlangeWidth>(empty).unwrap_err();
assert!(error
.to_string()
.contains("widths must contain at least one pair"));
assert!(super::SheetMetalFlangeEdgeWidths::new(Vec::new()).is_err());
}
#[test]
fn sketch_binding_preserves_known_planar_space_without_geometry() {
for wire in [
serde_json::json!({"definition": "sketch", "sketch": {"space": "unresolved"}}),
serde_json::json!({"definition": "sketch", "sketch": {"space": "planar"}}),
serde_json::json!({
"definition": "sketch",
"sketch": {"space": "planar", "sketch": "test:model:sketch#1"}
}),
] {
let definition: super::FeatureDefinition = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(definition).unwrap(), wire);
}
for wire in [
serde_json::json!({
"definition": "sketch",
"sketch": {"space": "unresolved", "sketch": "test:model:sketch#1"}
}),
serde_json::json!({"definition": "sketch", "sketch": {"space": "spatial"}}),
] {
assert!(serde_json::from_value::<super::FeatureDefinition>(wire).is_err());
}
assert_ne!(
super::SketchFeatureBinding::Unresolved,
super::SketchFeatureBinding::Planar(None)
);
}
mod body_selection;
#[test]
fn body_selection_admission_rejects_invalid_members() {
use super::{BodySelection, GeneratedBodyRef, NativeSelections};
use crate::ids::FeatureInputTopologyId;
for names in [
vec![],
vec![" ".to_owned()],
vec!["a".to_owned(), "a".to_owned()],
] {
assert!(BodySelection::local(
names.clone(),
"native".into(),
&cadmpeg_test_support::service_decode_context(),
)
.expect("body selection admission")
.is_err());
assert!(NativeSelections::try_from(names).is_err());
}
assert!(BodySelection::local(
vec!["body".into()],
" ".into(),
&cadmpeg_test_support::service_decode_context(),
)
.expect("body selection admission")
.is_err());
let state = FeatureInputTopologyId::mint("test:model:feature-input#1").unwrap();
assert!(BodySelection::historical(
state,
vec![],
"native".into(),
&cadmpeg_test_support::service_decode_context()
)
.expect("selection storage is admitted")
.is_err());
assert!(GeneratedBodyRef::new(
super::FeatureId::mint("test:test:feature#1").unwrap(),
" ".into(),
&cadmpeg_test_support::service_decode_context(),
)
.expect("selection reference admission")
.is_err());
for value in [
serde_json::json!({"kind":"local","value":{"bodies":[],"native":"source"}}),
serde_json::json!({"kind":"local","value":{"bodies":["a","a"],"native":"source"}}),
serde_json::json!({"kind":"generated","value":{"bodies":[],"native":"source"}}),
serde_json::json!({"kind":"native_set","value":[" "]}),
] {
assert!(serde_json::from_value::<BodySelection>(value).is_err());
}
}
#[test]
fn topology_membership_admission() {
use super::{DistinctMembers, FeatureResultTopology};
let id = crate::ids::FeatureResultTopologyId::mint("test:model:feature-result#1").unwrap();
let feature = super::FeatureId::mint("test:test:feature#1").unwrap();
assert!(crate::features::FeatureResultMembers::new(
vec![],
vec![],
vec![],
vec![],
&cadmpeg_test_support::service_decode_context(),
"validate feature result members"
)
.expect("result membership admission")
.map(|members| FeatureResultTopology::new(id.clone(), feature.clone(), members, None))
.is_err());
assert!(crate::features::FeatureResultMembers::new(
vec![
cadmpeg_core::nonblank_literal!("a"),
cadmpeg_core::nonblank_literal!("a"),
],
vec![],
vec![],
vec![],
&cadmpeg_test_support::service_decode_context(),
"validate feature result members"
)
.expect("result membership admission")
.map(|members| FeatureResultTopology::new(id.clone(), feature.clone(), members, None))
.is_err());
assert!(serde_json::from_value::<super::SelectionMember>(
serde_json::json!({"kind": "body", "id": ""})
)
.is_err());
assert!(DistinctMembers::<String>::try_from(
vec!["a".into(), "a".into()],
&cadmpeg_test_support::service_decode_context()
)
.is_err());
assert!(
serde_json::from_value::<DistinctMembers<crate::ids::BodyId>>(serde_json::json!([
"test:model:body#1",
"test:model:body#1"
]))
.is_err()
);
assert!(serde_json::from_value::<FeatureResultTopology>(
serde_json::json!({"id":"test:model:feature-result#1","output_of":"test:test:feature#1"})
)
.is_err());
}
#[test]
fn primitive_dimensions_are_checked_at_construction_and_deserialization() {
use super::{PrimitiveSolid, PrimitiveSolidKind};
use serde_json::{json, Value};
let cases: [(Value, &[&str]); 8] = [
(
json!({"kind":"box","length":1.0,"width":2.0,"height":3.0}),
&["length", "width", "height"],
),
(
json!({"kind":"cylinder","radius":1.0,"height":2.0,"angle":0.0}),
&["radius", "height"],
),
(
json!({"kind":"cone","radius1":0.0,"radius2":1.0,"height":2.0,"angle":-1.0}),
&["height"],
),
(
json!({"kind":"sphere","radius":1.0,"latitude1":-1.0,"latitude2":1.0,"longitude":0.0}),
&["radius"],
),
(
json!({"kind":"ellipsoid","x_radius":1.0,"y_radius":2.0,"z_radius":3.0,"latitude1":-1.0,"latitude2":1.0,"longitude":-1.0}),
&["x_radius", "y_radius", "z_radius"],
),
(
json!({"kind":"torus","major_radius":1.0,"minor_radius":2.0,"latitude1":-1.0,"latitude2":1.0,"longitude":0.0}),
&["major_radius", "minor_radius"],
),
(
json!({"kind":"prism","sides":3,"circumradius":1.0,"height":2.0}),
&["circumradius", "height"],
),
(
json!({"kind":"wedge","xmin":-1.0,"ymin":-1.0,"zmin":-1.0,"x2min":0.0,"z2min":0.0,"xmax":1.0,"ymax":1.0,"zmax":1.0,"x2max":0.0,"z2max":0.0}),
&[],
),
];
for (wire, positive_fields) in cases {
let kind: PrimitiveSolidKind = serde_json::from_value(wire.clone()).unwrap();
let solid = PrimitiveSolid::new(kind).unwrap();
assert_eq!(serde_json::to_value(&solid).unwrap(), wire);
assert_eq!(
serde_json::from_value::<PrimitiveSolid>(wire.clone()).unwrap(),
solid
);
let mut invalid = Vec::new();
for field in positive_fields {
for value in [0.0, -1.0] {
let mut changed = wire.clone();
changed[field] = json!(value);
invalid.push(changed);
}
}
match wire["kind"].as_str().unwrap() {
"cone" => {
let mut both_zero = wire.clone();
both_zero["radius2"] = json!(0.0);
invalid.push(both_zero);
for field in ["radius1", "radius2"] {
let mut changed = wire.clone();
changed[field] = json!(-1.0);
invalid.push(changed);
}
}
"sphere" | "ellipsoid" | "torus" => {
for lower in [1.0, 2.0] {
let mut changed = wire.clone();
changed["latitude1"] = json!(lower);
invalid.push(changed);
}
}
"prism" => {
for sides in [0, 1, 2] {
let mut changed = wire.clone();
changed["sides"] = json!(sides);
invalid.push(changed);
}
}
"wedge" => {
for field in ["xmax", "ymax", "zmax", "x2max", "z2max"] {
let mut changed = wire.clone();
changed[field] = json!(-1.0);
invalid.push(changed);
}
}
_ => {}
}
for changed in invalid {
let kind: PrimitiveSolidKind = serde_json::from_value(changed.clone()).unwrap();
assert!(PrimitiveSolid::new(kind).is_err(), "{changed}");
assert!(
serde_json::from_value::<PrimitiveSolid>(changed.clone()).is_err(),
"{changed}"
);
}
}
}
#[test]
fn scale_factors_admit_only_finite_nonzero_components() {
use crate::{features::ScaleFactors, scalar::NonZeroReal};
for value in [0.0, -0.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(NonZeroReal::new(value).is_none());
}
for value in [-2.0, 0.5] {
let factors = ScaleFactors::Uniform {
factor: NonZeroReal::new(value).unwrap(),
};
let wire = serde_json::to_value(factors).unwrap();
assert_eq!(
wire,
serde_json::json!({"kind": "uniform", "factor": value})
);
assert_eq!(
serde_json::from_value::<ScaleFactors>(wire).unwrap(),
factors
);
}
let factors = ScaleFactors::PerAxis {
factors: [-1.0, 2.0, -3.0].map(|value| NonZeroReal::new(value).unwrap()),
};
let wire = serde_json::to_value(factors).unwrap();
assert_eq!(
wire,
serde_json::json!({"kind": "per_axis", "factors": [-1.0, 2.0, -3.0]})
);
assert_eq!(
serde_json::from_value::<ScaleFactors>(wire).unwrap(),
factors
);
assert_eq!(
serde_json::to_value(ScaleFactors::Unresolved {}).unwrap(),
serde_json::json!({"kind": "unresolved"})
);
for wire in [
serde_json::json!({"kind": "uniform", "factor": 0.0}),
serde_json::json!({"kind": "per_axis", "factors": [0.0, 1.0, 1.0]}),
serde_json::json!({"kind": "per_axis", "factors": [1.0, 0.0, 1.0]}),
serde_json::json!({"kind": "per_axis", "factors": [1.0, 1.0, 0.0]}),
serde_json::json!({"kind": "per_axis", "factors": [1.0, 1.0]}),
serde_json::json!({"kind": "uniform", "factor": 1.0, "factors": [1.0, 1.0, 1.0]}),
serde_json::json!({"kind": "unresolved", "factor": 1.0}),
serde_json::json!({"uniform": 1.0}),
serde_json::json!({"x": 1.0, "y": 1.0, "z": 1.0}),
] {
assert!(serde_json::from_value::<ScaleFactors>(wire).is_err());
}
}
#[test]
fn blind_and_coil_lengths_reject_zero_without_losing_signed_wire_values() {
use crate::features::{CoilExtent, LinearTermination};
for value in [-4.0, 4.0] {
let wire = serde_json::json!({"kind": "blind", "length": value});
let admitted: LinearTermination = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(admitted).unwrap(), wire);
}
for value in [-0.0, 0.0] {
assert!(serde_json::from_value::<LinearTermination>(
serde_json::json!({"kind":"blind","length":value})
)
.is_err());
for wire in [
serde_json::json!({"kind":"revolutions_pitch","revolutions":1.0,"pitch":value}),
serde_json::json!({"kind":"height_pitch","height":value,"pitch":1.0}),
serde_json::json!({"kind":"height_pitch","height":1.0,"pitch":value}),
serde_json::json!({"kind":"spiral","revolutions":1.0,"radial_pitch":value}),
] {
assert!(serde_json::from_value::<CoilExtent>(wire).is_err());
}
}
let wire = serde_json::json!({"kind":"revolutions_height","revolutions":1.0,"height":0.0});
let admitted: CoilExtent = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(admitted).unwrap(), wire);
}
#[test]
fn hole_profile_filters_admit_exactly_the_nonempty_family_sets() {
use crate::features::holes::HoleProfileFilter;
let filters = [
(HoleProfileFilter::Points, "points"),
(HoleProfileFilter::Circles, "circles"),
(HoleProfileFilter::PointsAndCircles, "points_and_circles"),
(HoleProfileFilter::Arcs, "arcs"),
(HoleProfileFilter::PointsAndArcs, "points_and_arcs"),
(HoleProfileFilter::CirclesAndArcs, "circles_and_arcs"),
(HoleProfileFilter::All, "all"),
];
for (filter, name) in filters {
let wire = serde_json::json!(name);
assert_eq!(serde_json::to_value(filter).unwrap(), wire);
assert_eq!(
serde_json::from_value::<HoleProfileFilter>(wire).unwrap(),
filter
);
}
assert!(serde_json::from_value::<HoleProfileFilter>(serde_json::json!("none")).is_err());
assert!(serde_json::from_value::<HoleProfileFilter>(
serde_json::json!({"points":false,"circles":false,"arcs":false})
)
.is_err());
}
#[test]
fn polygon_side_counts_reject_degenerate_polygons_at_admission() {
use crate::features::PolygonSideCount;
for value in [0, 1, 2] {
assert!(PolygonSideCount::new(value).is_none());
assert!(serde_json::from_value::<PolygonSideCount>(serde_json::json!(value)).is_err());
}
for value in [3, 7, u32::MAX] {
let sides = PolygonSideCount::new(value).unwrap();
assert_eq!(sides.0, value);
assert_eq!(
serde_json::to_value(sides).unwrap(),
serde_json::json!(value)
);
assert_eq!(
serde_json::from_value::<PolygonSideCount>(serde_json::json!(value)).unwrap(),
sides
);
}
}
#[test]
fn feature_geometry_admission_preserves_nonunit_directions_and_zero_displacements() {
use crate::features::{FeatureDirection3, FinitePoint3, FiniteVector3};
for point in [Point3::new(0.0, 0.0, 0.0), Point3::new(-1.0, 2.0, f64::MAX)] {
let admitted = FinitePoint3::new(point).unwrap();
let wire = serde_json::to_value(point).unwrap();
assert_eq!(serde_json::to_value(admitted).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FinitePoint3>(wire).unwrap(),
admitted
);
}
for vector in [
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(-1.0, 2.0, f64::MAX),
] {
let admitted = FiniteVector3::new(vector).unwrap();
let wire = serde_json::to_value(vector).unwrap();
assert_eq!(serde_json::to_value(admitted).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FiniteVector3>(wire).unwrap(),
admitted
);
}
for vector in [Vector3::new(0.0, 0.0, -2.0), Vector3::new(3.0, 4.0, 0.0)] {
let admitted = FeatureDirection3::new(vector).unwrap();
let wire = serde_json::to_value(vector).unwrap();
assert_eq!(serde_json::to_value(admitted).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDirection3>(wire).unwrap(),
admitted
);
}
for vector in [
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(f64::MAX, 0.0, 0.0),
] {
assert!(FeatureDirection3::new(vector).is_none());
assert!(
serde_json::from_value::<FeatureDirection3>(serde_json::to_value(vector).unwrap())
.is_err()
);
}
for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
for axis in 0..3 {
let mut components = [0.0; 3];
components[axis] = invalid;
assert!(FinitePoint3::new(Point3::from(components)).is_none());
assert!(FiniteVector3::new(Vector3::from(components)).is_none());
assert!(FeatureDirection3::new(Vector3::from(components)).is_none());
let deserialize_point = || {
serde::de::value::MapDeserializer::<_, serde::de::value::Error>::new(
[
("x", components[0]),
("y", components[1]),
("z", components[2]),
]
.into_iter(),
)
};
assert!(
<FinitePoint3 as serde::Deserialize>::deserialize(deserialize_point()).is_err()
);
assert!(
<FiniteVector3 as serde::Deserialize>::deserialize(deserialize_point()).is_err()
);
assert!(
<FeatureDirection3 as serde::Deserialize>::deserialize(deserialize_point())
.is_err()
);
}
}
}
#[test]
fn a_point_from_finite_coordinates_holds_them_bit_for_bit() {
use crate::features::FinitePoint3;
use crate::scalar::FiniteReal;
for [x, y, z] in [[0.0, -0.0, 1.0], [f64::MAX, f64::MIN, 5.0e-324]] {
let point = FinitePoint3::from_coordinates(
FiniteReal::new(x).unwrap(),
FiniteReal::new(y).unwrap(),
FiniteReal::new(z).unwrap(),
);
assert_eq!(
[point.x, point.y, point.z].map(f64::to_bits),
[x, y, z].map(f64::to_bits)
);
assert_eq!(point, FinitePoint3::new(Point3::new(x, y, z)).unwrap());
}
}
#[test]
fn feature_lines_and_polylines_close_geometry_bounds_without_changing_wire_fields() {
use crate::features::{
FeatureDefinition, FeatureLineSegment, FeatureOperation, FeaturePolyline,
};
let first = Point3::new(0.0, 1.0, 2.0);
let second = Point3::new(3.0, 4.0, 5.0);
let segment = FeatureLineSegment::new(first, second).unwrap();
let wire =
serde_json::json!({"definition":"line_segment", "segment":{"start":first, "end":second}});
let definition = FeatureDefinition::Operation(FeatureOperation::LineSegment { segment });
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire).unwrap(),
definition
);
assert!(FeatureLineSegment::new(first, first).is_none());
assert!(FeatureLineSegment::new(Point3::new(f64::NAN, 0.0, 0.0), second).is_none());
assert!(serde_json::from_value::<FeatureDefinition>(
serde_json::json!({"definition":"line_segment", "segment":{"start":first, "end":first}})
)
.is_err());
for (points, closed) in [
(vec![first, second], false),
(vec![first, second, first], true),
(vec![first, second, first], false),
] {
let chain = FeaturePolyline::new(points.clone(), closed).unwrap();
let admitted = points
.iter()
.copied()
.map(crate::features::FinitePoint3::new)
.collect::<Option<Vec<_>>>()
.unwrap();
assert_eq!(
FeaturePolyline::from_parts(admitted, closed),
Some(chain.clone())
);
let wire = serde_json::json!({"definition":"polyline", "chain":{"points":points, "closed":closed}});
let definition = FeatureDefinition::Operation(FeatureOperation::Polyline { chain });
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire).unwrap(),
definition
);
}
for (points, closed) in [
(vec![], false),
(vec![first], false),
(vec![first, second], true),
(vec![first, first, second], false),
] {
assert!(FeaturePolyline::new(points.clone(), closed).is_none());
assert!(serde_json::from_value::<FeatureDefinition>(
serde_json::json!({"definition":"polyline", "chain":{"points":points, "closed":closed}})
)
.is_err());
}
assert!(
FeaturePolyline::new(vec![first, Point3::new(f64::INFINITY, 0.0, 0.0)], false).is_none()
);
}
#[test]
fn equation_curve_admission_preserves_expression_text_and_requires_an_increasing_domain() {
use crate::features::{FeatureDefinition, FeatureEquationCurve, FeatureOperation};
let curve = FeatureEquationCurve::new(
" t ".into(),
" t*t ".into(),
"0".into(),
" -t ".into(),
-2.0,
3.0,
)
.unwrap();
let definition = FeatureDefinition::Operation(FeatureOperation::EquationCurve { curve });
let wire = serde_json::json!({"definition":"equation_curve", "curve":{"parameter":" t ", "x_expression":" t*t ", "y_expression":"0", "z_expression":" -t ", "start":-2.0, "end":3.0}});
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire.clone()).unwrap(),
definition
);
for field in ["parameter", "x_expression", "y_expression", "z_expression"] {
for blank in ["", " \t\n"] {
let mut invalid = wire.clone();
invalid[field] = serde_json::json!(blank);
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
}
}
for [start, end] in [
[1.0, 1.0],
[2.0, 1.0],
[f64::NAN, 2.0],
[0.0, f64::INFINITY],
[f64::NEG_INFINITY, 0.0],
] {
assert!(FeatureEquationCurve::new(
"t".into(),
"t".into(),
"0".into(),
"0".into(),
start,
end
)
.is_none());
}
for [start, end] in [[1.0, 1.0], [2.0, 1.0]] {
let mut invalid = wire.clone();
invalid["curve"]["start"] = serde_json::json!(start);
invalid["curve"]["end"] = serde_json::json!(end);
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
}
}
#[test]
fn feature_arcs_preserve_directed_spans_and_admit_only_valid_frames_and_radii() {
use crate::geometry::DirectedParameterRange;
use crate::{
features::{FeatureCircularArc, FeatureDefinition, FeatureEllipticArc, FeatureOperation},
scalar::PositiveLength,
};
let center = Point3::new(1.0, 2.0, 3.0);
let normal = Vector3::new(0.0, 0.0, 2.0);
let major_axis = Vector3::new(3.0, 0.0, 0.0);
let major = PositiveLength::new(4.0).unwrap();
let minor = PositiveLength::new(2.0).unwrap();
for endpoints in [[0.0, std::f64::consts::TAU], [2.0, -1.0]] {
let angles = DirectedParameterRange::new(endpoints).unwrap();
let arc = FeatureCircularArc::new(center, normal, major, angles).unwrap();
let definition = FeatureDefinition::Operation(FeatureOperation::CircularArc { arc });
let wire = serde_json::json!({"definition":"circular_arc", "arc":{"center":center, "normal":normal, "radius":4.0, "start_angle":endpoints[0], "end_angle":endpoints[1]}});
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire.clone()).unwrap(),
definition
);
let mut invalid = wire;
invalid["arc"]["end_angle"] = invalid["arc"]["start_angle"].clone();
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
let arc =
FeatureEllipticArc::new(center, normal, major_axis, [major, minor], angles).unwrap();
let definition = FeatureDefinition::Operation(FeatureOperation::EllipticArc { arc });
let wire = serde_json::json!({"definition":"elliptic_arc", "arc":{"center":center, "normal":normal, "major_axis":major_axis, "major_radius":4.0, "minor_radius":2.0, "start_angle":endpoints[0], "end_angle":endpoints[1]}});
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire.clone()).unwrap(),
definition
);
for (field, value) in [
("minor_radius", serde_json::json!(5.0)),
("major_axis", serde_json::json!({"x":0.0,"y":0.0,"z":1.0})),
("end_angle", serde_json::json!(endpoints[0])),
] {
let mut invalid = wire.clone();
invalid["arc"][field] = value;
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
}
}
let angles = DirectedParameterRange::new([0.0, 1.0]).unwrap();
assert!(FeatureCircularArc::new(center, Vector3::new(0.0, 0.0, 0.0), major, angles).is_none());
assert!(FeatureEllipticArc::new(center, normal, major_axis, [minor, major], angles).is_none());
assert!(FeatureEllipticArc::new(center, normal, major_axis, [major, major], angles).is_some());
assert!(FeatureEllipticArc::new(center, normal, normal, [major, minor], angles).is_none());
assert!(FeatureEllipticArc::new(
center,
normal,
Vector3::new(1.0, 0.0, super::EPS_FEATURE_ELLIPSE_AXES_ORTHO / 2.0),
[major, minor],
angles
)
.is_some());
assert!(FeatureEllipticArc::new(
center,
normal,
Vector3::new(1.0, 0.0, super::EPS_FEATURE_ELLIPSE_AXES_ORTHO),
[major, minor],
angles
)
.is_none());
}
#[test]
fn block_placement_admission_requires_a_right_handed_rigid_transform() {
use crate::features::FeatureRigidPlacement;
use crate::transform::Transform;
for rows in [
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
[
[0.0, -1.0, 0.0, 3.0],
[1.0, 0.0, 0.0, -2.0],
[0.0, 0.0, 1.0, 5.0],
],
] {
let transform = Transform::affine(rows).unwrap();
let placement = FeatureRigidPlacement::new(transform).unwrap();
let wire = serde_json::to_value(transform).unwrap();
assert_eq!(serde_json::to_value(placement).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureRigidPlacement>(wire).unwrap(),
placement
);
}
for rows in [
[
[2.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
[
[1.0, 0.25, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
[
[-1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
] {
let transform = Transform::affine(rows).unwrap();
assert!(FeatureRigidPlacement::new(transform).is_none());
assert!(serde_json::from_value::<FeatureRigidPlacement>(
serde_json::to_value(transform).unwrap()
)
.is_err());
}
}
#[test]
fn helical_sweep_travel_preserves_signed_and_planar_values_but_rejects_zero_travel() {
use crate::{features::HelicalSweepTravel, scalar::Length};
for (height, radial_growth, wire) in [
(-2.0, 0.0, serde_json::json!({"kind":"axial","height":-2.0})),
(
0.0,
-3.0,
serde_json::json!({"kind":"radial","radial_growth":-3.0}),
),
(
2.0,
-3.0,
serde_json::json!({"kind":"conical","height":2.0,"radial_growth":-3.0}),
),
] {
let travel = HelicalSweepTravel::new(
Length::new(height).unwrap(),
Length::new(radial_growth).unwrap(),
)
.unwrap();
assert_eq!(travel.height().get(), height);
assert_eq!(travel.radial_growth().get(), radial_growth);
assert_eq!(serde_json::to_value(travel).unwrap(), wire);
assert_eq!(
serde_json::from_value::<HelicalSweepTravel>(wire).unwrap(),
travel
);
}
assert!(HelicalSweepTravel::new(Length::ZERO, Length::ZERO).is_none());
assert!(serde_json::from_value::<HelicalSweepTravel>(
serde_json::json!({"kind":"axial","height":0.0})
)
.is_err());
assert!(serde_json::from_value::<HelicalSweepTravel>(
serde_json::json!({"kind":"conical","height":1.0,"radial_growth":0.0})
)
.is_err());
assert!(serde_json::from_value::<HelicalSweepTravel>(
serde_json::json!({"kind":"axial","height":1.0,"radial_growth":2.0})
)
.is_err());
}
#[test]
fn feature_coordinate_frame_admission_preserves_wire_and_handedness_bound() {
use crate::features::{
FeatureCoordinateFrame, FeatureDefinition, FeatureOperation, EPS_FEATURE_UNIT_FRAME,
};
use crate::math::{Point3, Vector3};
let origin = Point3::new(1.0, 2.0, 3.0);
let x = Vector3::new(1.0, 0.0, 0.0);
let y = Vector3::new(0.0, 1.0, 0.0);
let z = Vector3::new(0.0, 0.0, 1.0);
let frame = FeatureCoordinateFrame::new(origin, x, y, z).unwrap();
let wire = serde_json::json!({"definition":"datum_coordinate_system","frame":{"origin":origin,"x_axis":x,"y_axis":y,"z_axis":z}});
let definition =
FeatureDefinition::Operation(FeatureOperation::DatumCoordinateSystem { frame });
assert_eq!(serde_json::to_value(&definition).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(wire.clone()).unwrap(),
definition
);
for (key, value) in [
("x_axis", y),
("z_axis", Vector3::new(0.0, 0.0, -1.0)),
("x_axis", Vector3::new(2.0, 0.0, 0.0)),
] {
let mut invalid = wire.clone();
invalid["frame"][key] = serde_json::to_value(value).unwrap();
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
}
assert!(FeatureCoordinateFrame::new(Point3::new(f64::NAN, 0.0, 0.0), x, y, z).is_none());
let expanded = 1.0 + EPS_FEATURE_UNIT_FRAME * 0.75;
let frame = FeatureCoordinateFrame::new(
origin,
Vector3::new(expanded, 0.0, 0.0),
Vector3::new(0.0, expanded, 0.0),
Vector3::new(0.0, 0.0, expanded),
)
.unwrap();
assert!(
frame
.x_axis()
.as_raw()
.cross(*frame.y_axis().as_raw())
.dot(*frame.z_axis().as_raw())
> 1.0 + EPS_FEATURE_UNIT_FRAME
);
}
#[test]
fn checked_feature_frame_parts_preserve_wire_and_cross_field_refusal() {
use crate::features::{
FeatureCoordinateFrame, FeatureDatumPlaneFrame, FeatureDirection3,
FeatureSupportPlaneFrame, FeatureUnitPlaneFrame, FinitePoint3,
};
use crate::units::UnitVector3;
let origin = Point3::new(1.0, 2.0, 3.0);
let x = Vector3::new(1.0, 0.0, 0.0);
let y = Vector3::new(0.0, 1.0, 0.0);
let z = Vector3::new(0.0, 0.0, 1.0);
let admitted_origin = FinitePoint3::new(origin).unwrap();
let plane = FeatureUnitPlaneFrame::from_parts(
admitted_origin,
UnitVector3::new(x).unwrap(),
UnitVector3::new(y).unwrap(),
)
.unwrap();
let frame = FeatureCoordinateFrame::from_parts(plane, UnitVector3::new(z).unwrap()).unwrap();
assert_eq!(frame, FeatureCoordinateFrame::new(origin, x, y, z).unwrap());
assert!(FeatureCoordinateFrame::from_parts(plane, UnitVector3::new(x).unwrap()).is_none());
let normal = Vector3::new(0.0, 0.0, 2.0);
let u_axis = Vector3::new(-3.0, 0.0, 0.0);
let admitted_normal = FeatureDirection3::new(normal).unwrap();
let admitted_u_axis = FeatureDirection3::new(u_axis).unwrap();
let datum =
FeatureDatumPlaneFrame::from_parts(admitted_origin, admitted_normal, admitted_u_axis)
.unwrap();
let support =
FeatureSupportPlaneFrame::from_parts(admitted_origin, admitted_normal, admitted_u_axis)
.unwrap();
assert_eq!(
datum,
FeatureDatumPlaneFrame::new(origin, normal, u_axis).unwrap()
);
assert_eq!(
support,
FeatureSupportPlaneFrame::new(origin, normal, u_axis).unwrap()
);
let parallel = FeatureDirection3::new(Vector3::new(0.0, 0.0, 1.0)).unwrap();
assert!(
FeatureDatumPlaneFrame::from_parts(admitted_origin, admitted_normal, parallel).is_none()
);
assert!(
FeatureSupportPlaneFrame::from_parts(admitted_origin, admitted_normal, parallel).is_none()
);
}
#[test]
fn feature_unit_plane_and_image_bounds_reject_degenerate_geometry() {
use crate::features::{FeatureImageBounds, FeatureUnitPlaneFrame, EPS_FEATURE_UNIT_FRAME};
use crate::math::{Point2, Point3, Vector3};
let origin = Point3::new(0.0, 0.0, 0.0);
let x = Vector3::new(1.0, 0.0, 0.0);
for other in [
Vector3::new(0.0, 0.0, 0.0),
x,
Vector3::new(f64::INFINITY, 0.0, 0.0),
] {
assert!(FeatureUnitPlaneFrame::new(origin, x, other).is_none());
}
assert!(FeatureUnitPlaneFrame::new(
origin,
x,
Vector3::new(EPS_FEATURE_UNIT_FRAME * 0.5, 1.0, 0.0)
)
.is_some());
assert!(FeatureUnitPlaneFrame::new(
origin,
x,
Vector3::new(EPS_FEATURE_UNIT_FRAME * 2.0, 1.0, 0.0)
)
.is_none());
assert!(serde_json::from_value::<FeatureUnitPlaneFrame>(
serde_json::json!({"origin":origin,"u_axis":x,"v_axis":x})
)
.is_err());
for corners in [
[Point2::new(2.0, 3.0), Point2::new(-2.0, -3.0)],
[Point2::new(-2.0, 3.0), Point2::new(2.0, -3.0)],
] {
let bounds = FeatureImageBounds::new(corners).unwrap();
assert_eq!(bounds.corners(), corners);
assert_eq!(
bounds.corners(),
corners.map(|corner| crate::units::FinitePoint2::new(corner).unwrap())
);
let wire = serde_json::to_value(corners).unwrap();
assert_eq!(serde_json::to_value(bounds).unwrap(), wire);
assert_eq!(
serde_json::from_value::<FeatureImageBounds>(wire).unwrap(),
bounds
);
}
for corners in [
[Point2::new(0.0, 0.0), Point2::new(0.0, 1.0)],
[Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)],
] {
assert!(FeatureImageBounds::new(corners).is_none());
assert!(serde_json::from_value::<FeatureImageBounds>(
serde_json::to_value(corners).unwrap()
)
.is_err());
}
assert!(FeatureImageBounds::new([Point2::new(f64::NAN, 0.0), Point2::new(1.0, 1.0)]).is_none());
}
#[test]
fn reference_image_and_coil_frames_preserve_wire_fields_and_reject_invalid_frames() {
use crate::features::{CoilPlacement, FeatureDefinition};
let image = serde_json::json!({
"definition":"reference_image", "asset":"synthetic:test:asset#frame",
"visible":true, "mirror_u":false, "mirror_v":false,
"frame":{
"origin":{"x":1.0,"y":2.0,"z":3.0},
"u_axis":{"x":1.0,"y":0.0,"z":0.0},
"v_axis":{"x":0.0,"y":1.0,"z":0.0}
},
"bounds":[{"u":2.0,"v":3.0},{"u":-2.0,"v":-3.0}]
});
let decoded = serde_json::from_value::<FeatureDefinition>(image.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), image);
let mut invalid = image.clone();
invalid["frame"]["v_axis"] = invalid["frame"]["u_axis"].clone();
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
let mut invalid = image;
invalid["bounds"][1]["u"] = serde_json::json!(2.0);
assert!(serde_json::from_value::<FeatureDefinition>(invalid).is_err());
let coil = serde_json::json!({"kind":"explicit",
"origin":{"x":1.0,"y":2.0,"z":3.0},
"axis":{"x":0.0,"y":0.0,"z":1.0},
"radial":{"x":1.0,"y":0.0,"z":0.0}
});
let decoded = serde_json::from_value::<CoilPlacement>(coil.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), coil);
let mut invalid = coil;
invalid["radial"] = invalid["axis"].clone();
assert!(serde_json::from_value::<CoilPlacement>(invalid).is_err());
assert!(serde_json::from_value::<CoilPlacement>(
serde_json::json!({"kind":"native","native_ref":" \t "})
)
.is_err());
let native = serde_json::json!({"kind":"native","native_ref":" source:placement "});
let decoded = serde_json::from_value::<CoilPlacement>(native.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), native);
}
#[test]
fn datum_and_support_plane_frames_preserve_nonunit_geometry_and_wire_fields() {
use crate::features::{
DatumPlaneReference, FeatureDatumPlaneFrame, FeatureDefinition, FeatureOperation,
FeatureSupportPlaneFrame,
};
let origin = Point3::new(1.0, 2.0, 3.0);
let normal = Vector3::new(0.0, 0.0, 2.0);
let u_axis = Vector3::new(-3.0, 0.0, 0.0);
let datum = FeatureDatumPlaneFrame::new(origin, normal, u_axis).unwrap();
let support = FeatureSupportPlaneFrame::new(origin, normal, u_axis).unwrap();
let geometry = serde_json::json!({"origin":origin,"normal":normal,"u_axis":u_axis});
assert_eq!(serde_json::to_value(datum).unwrap(), geometry);
assert_eq!(serde_json::to_value(support).unwrap(), geometry);
let datum_wire = serde_json::json!({"definition":"datum_plane","frame":geometry.clone()});
let definition = FeatureDefinition::Operation(FeatureOperation::DatumPlane { frame: datum });
assert_eq!(serde_json::to_value(&definition).unwrap(), datum_wire);
assert_eq!(
serde_json::from_value::<FeatureDefinition>(datum_wire).unwrap(),
definition
);
let resolved = DatumPlaneReference::ResolvedPlane { frame: support };
let resolved_wire =
serde_json::json!({"reference": "resolved_plane", "frame": geometry.clone()});
assert_eq!(serde_json::to_value(&resolved).unwrap(), resolved_wire);
assert_eq!(
serde_json::from_value::<DatumPlaneReference>(resolved_wire).unwrap(),
resolved
);
}
#[test]
fn plane_frame_admission_rejects_nonfinite_degenerate_and_nonorthogonal_directions() {
use crate::features::{FeatureDatumPlaneFrame, FeatureSupportPlaneFrame};
let origin = Point3::new(0.0, 0.0, 0.0);
let normal = Vector3::new(0.0, 0.0, 2.0);
for u_axis in [Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 1.0)] {
assert!(FeatureDatumPlaneFrame::new(origin, normal, u_axis).is_none());
assert!(FeatureSupportPlaneFrame::new(origin, normal, u_axis).is_none());
let wire = serde_json::json!({"origin":origin,"normal":normal,"u_axis":u_axis});
assert!(serde_json::from_value::<FeatureDatumPlaneFrame>(wire.clone()).is_err());
assert!(serde_json::from_value::<FeatureSupportPlaneFrame>(wire).is_err());
}
let u_axis = Vector3::new(3.0, 0.0, 0.0);
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(
FeatureDatumPlaneFrame::new(Point3::new(value, 0.0, 0.0), normal, u_axis).is_none()
);
assert!(
FeatureSupportPlaneFrame::new(origin, Vector3::new(0.0, 0.0, value), u_axis).is_none()
);
}
let small = f64::EPSILON / 2.0;
assert!(FeatureDatumPlaneFrame::new(origin, Vector3::new(0.0, 0.0, small), u_axis).is_some());
assert!(FeatureSupportPlaneFrame::new(origin, normal, Vector3::new(small, 0.0, 0.0)).is_some());
}
#[test]
fn plane_frame_owners_preserve_their_distinct_floating_point_thresholds() {
use crate::features::{
FeatureDatumPlaneFrame, FeatureSupportPlaneFrame, EPS_FEATURE_PLANE_ORTHOGONAL,
};
let origin = Point3::new(0.0, 0.0, 0.0);
let normal = Vector3::new(0.0, 0.0, 0.1);
let u_axis = Vector3::new(0.7, 0.0, EPS_FEATURE_PLANE_ORTHOGONAL * 0.7);
let dot = normal.dot(u_axis).abs();
let datum_bound = EPS_FEATURE_PLANE_ORTHOGONAL * (normal.norm() * u_axis.norm());
let support_bound = (EPS_FEATURE_PLANE_ORTHOGONAL * normal.norm()) * u_axis.norm();
assert!(dot > datum_bound);
assert!(dot <= support_bound);
assert!(FeatureDatumPlaneFrame::new(origin, normal, u_axis).is_none());
assert!(FeatureSupportPlaneFrame::new(origin, normal, u_axis).is_some());
}
#[test]
fn a_resolved_plane_reference_checks_the_geometry_it_carries() {
use crate::features::{DatumPlaneReference, FaceSelection};
let degenerate = serde_json::json!({
"reference": "resolved_plane",
"frame": {
"origin":{"x":0.0,"y":0.0,"z":0.0},
"normal":{"x":0.0,"y":0.0,"z":0.0},
"u_axis":{"x":1.0,"y":0.0,"z":0.0}
}
});
assert!(serde_json::from_value::<DatumPlaneReference>(degenerate).is_err());
let native = serde_json::json!({
"reference": "face",
"face": {"kind":"native","value":"face:retained"}
});
assert_eq!(
serde_json::from_value::<DatumPlaneReference>(native).unwrap(),
DatumPlaneReference::Face {
face: FaceSelection::Native("face:retained".into())
}
);
}
mod selections;
mod parameters;
mod configuration_states;
mod source_content;
mod profile_selections;
mod profile_regions;
mod flange_widths;
mod local_admission;
mod configurations;
mod finite_vectors;
mod unit_scaling;
mod wire_forms;
#[test]
fn every_payload_free_feature_variant_the_freecad_sweep_reached_refuses_an_unknown_key() {
use crate::features::{ExtrusionDirectionSource, FuzzyTolerance, SweepOrientation};
for kind in ["custom", "profile_normal"] {
let wire = serde_json::json!({"kind": kind, "zz_bogus": 1});
let error = serde_json::from_value::<ExtrusionDirectionSource>(wire)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{kind}: {error}");
}
for kind in ["corrected_frenet", "fixed", "frenet"] {
let wire = serde_json::json!({"kind": kind, "zz_bogus": 1});
let error = serde_json::from_value::<SweepOrientation>(wire)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{kind}: {error}");
}
for kind in ["kernel_default", "automatic"] {
let wire = serde_json::json!({"kind": kind, "zz_bogus": 1});
let error = serde_json::from_value::<FuzzyTolerance>(wire)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{kind}: {error}");
}
assert_eq!(
serde_json::to_value(ExtrusionDirectionSource::Custom {}).unwrap(),
serde_json::json!({"kind": "custom"})
);
assert_eq!(
serde_json::to_value(SweepOrientation::Frenet {}).unwrap(),
serde_json::json!({"kind": "frenet"})
);
assert_eq!(
serde_json::to_value(FuzzyTolerance::KernelDefault).unwrap(),
serde_json::json!({"kind": "kernel_default"})
);
}
#[test]
fn a_hole_drilling_direction_admits_only_a_finite_nonzero_vector() {
use crate::features::holes::{HoleConstruction, HoleKind, HoleShape};
use crate::features::{FeatureDirection3, FeatureOperation};
for vector in [
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(f64::NAN, 0.0, 1.0),
Vector3::new(f64::INFINITY, 0.0, 1.0),
Vector3::new(0.0, f64::NEG_INFINITY, 1.0),
] {
assert!(FeatureDirection3::new(vector).is_none(), "{vector:?}");
}
let direction = FeatureDirection3::new(Vector3::new(0.0, 0.0, -2.0)).unwrap();
let shape = HoleShape::new(
HoleConstruction::Form {
kind: HoleKind::Simple,
specification: None,
},
None,
None,
)
.unwrap();
let hole = FeatureOperation::Hole {
profile: None,
profile_filter: None,
face: None,
direction: Some(direction),
placements: None,
shape,
extent: None,
bottom: None,
taper_angle: None,
allow_multi_profile_faces: None,
};
assert!(matches!(
hole,
FeatureOperation::Hole {
direction: Some(value),
..
} if value.get() == Vector3::new(0.0, 0.0, -2.0)
));
}
#[test]
fn a_hole_wire_refuses_a_degenerate_drilling_direction() {
use crate::features::{FeatureDefinition, FeatureOperation};
let wire = |direction: serde_json::Value| {
serde_json::json!({
"definition": "hole",
"shape": {"construction": {"construction": "form", "kind": {"kind": "simple"}}},
"direction": direction
})
};
let degenerate = wire(serde_json::json!({"x": 0.0, "y": 0.0, "z": 0.0}));
let error = serde_json::from_value::<FeatureOperation>(degenerate.clone())
.unwrap_err()
.to_string();
assert!(error.contains("FeatureDirection3 norm"), "{error}");
assert!(serde_json::from_value::<FeatureDefinition>(degenerate).is_err());
let admitted = wire(serde_json::json!({"x": 0.0, "y": 0.0, "z": -2.0}));
let definition: FeatureDefinition = serde_json::from_value(admitted.clone()).unwrap();
assert!(matches!(
definition,
FeatureDefinition::Operation(FeatureOperation::Hole {
direction: Some(_),
..
})
));
assert_eq!(serde_json::to_value(&definition).unwrap(), admitted);
}
#[test]
fn feature_direction_from_unit_vector_keeps_the_admitted_components() {
use crate::features::FeatureDirection3;
use crate::units::{OrthonormalFrame3, UnitVector3};
for vector in [
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.6, -0.8, 0.0),
Vector3::new(1.0 + 0.5e-9, 0.0, 0.0),
Vector3::new(0.0, -(1.0 - 0.5e-9), 0.0),
] {
let unit = UnitVector3::new(vector).unwrap();
for (unit, expected) in [
(unit, vector),
(
unit.reversed(),
Vector3::new(-vector.x, -vector.y, -vector.z),
),
] {
let direction = FeatureDirection3::from(unit);
assert_eq!(
[direction.x, direction.y, direction.z].map(f64::to_bits),
[expected.x, expected.y, expected.z].map(f64::to_bits)
);
assert_eq!(FeatureDirection3::new(expected), Some(direction));
}
}
let frame =
OrthonormalFrame3::new(Vector3::new(0.0, 0.0, 1.0), Vector3::new(1.0, 0.0, 0.0)).unwrap();
assert_eq!(
FeatureDirection3::from(*frame.axis()).get(),
*frame.axis().as_raw()
);
assert_eq!(
FeatureDirection3::from(*frame.reference()).get(),
*frame.reference().as_raw()
);
}
#[test]
fn feature_direction_reversed_negates_the_components_and_stays_admitted() {
use crate::features::FeatureDirection3;
for vector in [
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.6, -0.8, 0.0),
Vector3::new(-0.0, 3.0, -4.0),
Vector3::new(1.0e154, 0.0, -0.0),
Vector3::new(1.0e-160, 0.0, 0.0),
Vector3::new(-2.5, 1.0e-300, 7.0e100),
] {
let direction = FeatureDirection3::new(vector).unwrap();
let reversed = direction.reversed();
assert_eq!(
[reversed.x, reversed.y, reversed.z].map(f64::to_bits),
[-vector.x, -vector.y, -vector.z].map(f64::to_bits)
);
assert_eq!(FeatureDirection3::new(reversed.get()), Some(reversed));
assert_eq!(
reversed.dot(reversed.get()).to_bits(),
direction.dot(direction.get()).to_bits()
);
let restored = reversed.reversed();
assert_eq!(
[restored.x, restored.y, restored.z].map(f64::to_bits),
[vector.x, vector.y, vector.z].map(f64::to_bits)
);
}
}
#[test]
fn finite_point_negated_negates_the_coordinates_and_stays_admitted() {
use crate::features::FinitePoint3;
for point in [
Point3::new(0.0, 0.0, 0.0),
Point3::new(-0.0, 1.0, -2.5),
Point3::new(f64::MAX, -f64::MAX, f64::MIN_POSITIVE),
Point3::new(5.0e-324, -1.0e300, 3.0),
] {
let admitted = FinitePoint3::new(point).unwrap();
let negated = admitted.negated();
assert_eq!(
[negated.x, negated.y, negated.z].map(f64::to_bits),
[-point.x, -point.y, -point.z].map(f64::to_bits)
);
assert_eq!(FinitePoint3::new(negated.get()), Some(negated));
let restored = negated.negated();
assert_eq!(
[restored.x, restored.y, restored.z].map(f64::to_bits),
[point.x, point.y, point.z].map(f64::to_bits)
);
}
}
#[test]
fn feature_frames_replace_the_origin_and_keep_the_admitted_axes() {
use crate::features::{
FeatureCoordinateFrame, FeatureDatumPlaneFrame, FeatureSupportPlaneFrame,
FeatureUnitPlaneFrame, FinitePoint3,
};
let origin = Point3::new(1.0, 2.0, 3.0);
let moved = Point3::new(-4.0, 5.0e-324, f64::MAX);
let moved_origin = FinitePoint3::new(moved).unwrap();
let x = Vector3::new(0.6, 0.8, 0.0);
let y = Vector3::new(-0.8, 0.6, 0.0);
let z = Vector3::new(0.0, 0.0, 1.0);
let unit = FeatureUnitPlaneFrame::new(origin, x, y).unwrap();
assert_eq!(
unit.with_origin(moved_origin),
FeatureUnitPlaneFrame::new(moved, x, y).unwrap()
);
let coordinate = FeatureCoordinateFrame::new(origin, x, y, z).unwrap();
assert_eq!(
coordinate.with_origin(moved_origin),
FeatureCoordinateFrame::new(moved, x, y, z).unwrap()
);
let normal = Vector3::new(0.0, 0.0, 2.0);
let u_axis = Vector3::new(3.0, 0.0, 0.0);
let datum = FeatureDatumPlaneFrame::new(origin, normal, u_axis).unwrap();
let moved_datum = datum.with_origin(moved_origin);
assert_eq!(
moved_datum,
FeatureDatumPlaneFrame::new(moved, normal, u_axis).unwrap()
);
assert_eq!(moved_datum.normal(), normal);
assert_eq!(moved_datum.u_axis(), u_axis);
let support = FeatureSupportPlaneFrame::new(origin, normal, u_axis).unwrap();
assert_eq!(
support.with_origin(moved_origin),
FeatureSupportPlaneFrame::new(moved, normal, u_axis).unwrap()
);
assert_eq!(
[
moved_datum.origin().x,
moved_datum.origin().y,
moved_datum.origin().z
]
.map(f64::to_bits),
[moved.x, moved.y, moved.z].map(f64::to_bits)
);
}
#[test]
fn feature_arcs_rebuild_from_checked_parts() {
use crate::features::{
FeatureCircularArc, FeatureDirection3, FeatureEllipticArc, FinitePoint3,
};
use crate::geometry::DirectedParameterRange;
use crate::scalar::{PositiveLength, PositiveReal};
let center = Point3::new(1.0, 2.0, 3.0);
let normal = Vector3::new(0.0, 0.0, 2.0);
let major_axis = Vector3::new(3.0, 0.0, 0.0);
let angles = DirectedParameterRange::new([2.0, -1.0]).unwrap();
let radius = PositiveLength::new(4.0).unwrap();
let arc = FeatureCircularArc::new(center, normal, radius, angles).unwrap();
assert_eq!(arc.center(), FinitePoint3::new(center).unwrap());
assert_eq!(arc.normal(), FeatureDirection3::new(normal).unwrap());
assert_eq!(
FeatureCircularArc::from_parts(arc.center(), arc.normal(), arc.radius(), arc.angles()),
arc
);
let moved = FinitePoint3::new(Point3::new(-5.0, 0.0, 7.5)).unwrap();
let wider = PositiveLength::new(8.0).unwrap();
assert_eq!(
FeatureCircularArc::from_parts(moved, arc.normal(), wider, arc.angles()),
FeatureCircularArc::new(moved.get(), normal, wider, angles).unwrap()
);
let [major, minor] = [4.0, 2.0].map(|value| PositiveLength::new(value).unwrap());
let ellipse =
FeatureEllipticArc::new(center, normal, major_axis, [major, minor], angles).unwrap();
let scale = PositiveReal::new(2.0).unwrap();
let scaled = [8.0, 4.0].map(|value| PositiveLength::new(value).unwrap());
assert_eq!(
ellipse
.with_scaled_radii(scale)
.map(|arc| arc.with_center(moved)),
FeatureEllipticArc::new(moved.get(), normal, major_axis, scaled, angles)
);
let equal =
FeatureEllipticArc::new(center, normal, major_axis, [major, major], angles).unwrap();
assert_eq!(
Some(equal.with_center(moved)),
FeatureEllipticArc::new(moved.get(), normal, major_axis, [major, major], angles)
);
assert!(
FeatureEllipticArc::new(moved.get(), normal, major_axis, [minor, major], angles).is_none()
);
}
#[test]
fn with_translation_keeps_the_linear_rows_and_the_rigid_admission() {
use crate::features::{FeatureRigidPlacement, FiniteVector3};
use crate::transform::Transform;
let (sine, cosine) = 0.6_f64.sin_cos();
let transform = Transform::affine([
[cosine, -sine, 0.0, 4.0],
[sine, cosine, 0.0, -2.0],
[0.0, 0.0, 1.0, 1.0],
])
.unwrap();
let translation = FiniteVector3::new(Vector3::new(-0.0, 5.0e-324, f64::MAX)).unwrap();
let moved = transform.with_translation(translation);
let mut expected = transform.affine_rows();
for (row, component) in expected.iter_mut().zip([-0.0, 5.0e-324, f64::MAX]) {
row[3] = component;
}
assert_eq!(
moved.affine_rows().map(|row| row.map(f64::to_bits)),
expected.map(|row| row.map(f64::to_bits))
);
assert_eq!(Transform::affine(expected), Some(moved));
let placement = FeatureRigidPlacement::new(transform).unwrap();
let placed = placement.with_translation(translation);
assert_eq!(FeatureRigidPlacement::new(moved), Some(placed));
}
#[test]
fn apply_point_hands_back_an_admitted_point_bit_for_bit() {
use crate::features::FinitePoint3;
use crate::transform::Transform;
let transform = Transform::affine([
[2.0, 0.5, 0.0, 4.0],
[0.0, 3.0, 0.0, -2.0],
[0.0, 0.0, 4.0, 1.0],
])
.unwrap();
for point in [
Point3::new(1.0, 2.0, 3.0),
Point3::new(-0.0, 5.0e-324, -1.0e300),
] {
let admitted = FinitePoint3::new(point).unwrap();
let placed = transform.apply_point(admitted.get()).unwrap();
let raw = transform.apply_point(point).unwrap().get();
assert_eq!(
[placed.x, placed.y, placed.z].map(f64::to_bits),
[raw.x, raw.y, raw.z].map(f64::to_bits)
);
assert_eq!(FinitePoint3::new(raw), Some(placed));
}
let overflow = Transform::affine([
[1.0, 0.0, 0.0, f64::MAX],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
])
.unwrap();
let edge = FinitePoint3::new(Point3::new(f64::MAX, 0.0, 0.0)).unwrap();
assert_eq!(overflow.apply_point(edge.get()), None);
assert_eq!(
overflow.apply_point(FinitePoint3::new(Point3::new(0.0, 0.0, 1.0)).unwrap().get()),
FinitePoint3::new(Point3::new(f64::MAX, 0.0, 1.0))
);
}
#[test]
fn apply_vector_hands_back_an_admitted_vector_bit_for_bit() {
use crate::features::FiniteVector3;
use crate::transform::Transform;
let transform = Transform::affine([
[2.0, 0.5, 0.0, 4.0],
[0.0, 3.0, 0.0, -2.0],
[0.0, 0.0, 4.0, 1.0],
])
.unwrap();
for vector in [
Vector3::new(1.0, 2.0, 3.0),
Vector3::new(-0.0, 5.0e-324, -1.0e300),
] {
let admitted = FiniteVector3::new(vector).unwrap();
let placed = transform.apply_vector(admitted.get()).unwrap();
let raw = transform.apply_vector(vector).unwrap().get();
assert_eq!(
[placed.x, placed.y, placed.z].map(f64::to_bits),
[raw.x, raw.y, raw.z].map(f64::to_bits)
);
assert_eq!(FiniteVector3::new(raw), Some(placed));
}
let stretch = Transform::affine([
[f64::MAX, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
])
.unwrap();
let unit = FiniteVector3::new(Vector3::new(1.0, 0.0, 0.0)).unwrap();
assert_eq!(
stretch.apply_vector(unit.get()),
FiniteVector3::new(Vector3::new(f64::MAX, 0.0, 0.0))
);
assert_eq!(
stretch.apply_vector(stretch.apply_vector(unit.get()).unwrap().get()),
None
);
}
#[test]
fn feature_geometry_constants_are_the_admitted_literals() {
use crate::features::{FeatureDirection3, FinitePoint3};
let origin = Point3::new(0.0, 0.0, 0.0);
assert_eq!(FinitePoint3::new(origin), Some(FinitePoint3::ZERO));
assert_eq!(
[
FinitePoint3::ZERO.x,
FinitePoint3::ZERO.y,
FinitePoint3::ZERO.z
]
.map(f64::to_bits),
[origin.x, origin.y, origin.z].map(f64::to_bits)
);
let z_axis = Vector3::new(0.0, 0.0, 1.0);
assert_eq!(
FeatureDirection3::new(z_axis),
Some(FeatureDirection3::Z_AXIS)
);
assert_eq!(
[
FeatureDirection3::Z_AXIS.x,
FeatureDirection3::Z_AXIS.y,
FeatureDirection3::Z_AXIS.z
]
.map(f64::to_bits),
[z_axis.x, z_axis.y, z_axis.z].map(f64::to_bits)
);
}
#[test]
fn wrap_depth_admits_only_a_positive_length_on_the_wire() {
use crate::features::FeatureOperation;
let wire = |depth: f64| {
serde_json::json!({
"definition": "wrap",
"profile": {"kind": "native", "value": "wrap:profile"},
"face": {"kind": "native", "value": "wrap:face"},
"mode": {"deboss": {"depth": depth}}
})
};
for refused in [0.0, -0.0, -2.5] {
let error = serde_json::from_value::<FeatureOperation>(wire(refused))
.unwrap_err()
.to_string();
assert!(error.contains("PositiveLength must be positive"), "{error}");
}
let admitted = wire(f64::MIN_POSITIVE);
let operation: FeatureOperation = serde_json::from_value(admitted.clone()).unwrap();
assert_eq!(serde_json::to_value(operation).unwrap(), admitted);
}
#[test]
fn feature_frames_hold_their_admitted_unit_axes() {
use crate::features::{
FeatureCoordinateFrame, FeatureDirection3, FeatureUnitPlaneFrame, FiniteVector3,
};
use crate::units::UnitVector3;
let origin = Point3::new(1.0, 2.0, 3.0);
let u = Vector3::new(0.0, 1.0, 0.0);
let v = Vector3::new(0.0, 0.0, 1.0);
let plane = FeatureUnitPlaneFrame::new(origin, u, v).unwrap();
assert_eq!(plane.u_axis(), UnitVector3::new(u).unwrap());
assert_eq!(plane.v_axis(), UnitVector3::new(v).unwrap());
assert_eq!(
serde_json::to_value(plane).unwrap(),
serde_json::json!({"origin": origin, "u_axis": u, "v_axis": v})
);
let frame = FeatureCoordinateFrame::new(
origin,
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
.unwrap();
assert_eq!(
[frame.x_axis(), frame.y_axis(), frame.z_axis()],
[
UnitVector3::X_AXIS,
UnitVector3::Y_AXIS,
UnitVector3::Z_AXIS
]
);
let direction = FeatureDirection3::new(Vector3::new(0.0, 3.0, 4.0)).unwrap();
assert_eq!(
FiniteVector3::from(direction),
FiniteVector3::new(Vector3::new(0.0, 3.0, 4.0)).unwrap()
);
}
mod reference_views;