use crate::geometry::{
ProceduralGeometryError, ProceduralSurfaceDefinition, RevisionCacheForm,
RevisionG2BlendConstruction, RevisionG2BlendConstructionWire, RevisionSurfaceParameterization,
RollingBallRadiusSelector, RollingBallSide, RollingBallSupportCurve, RollingBallSupportSurface,
VariableBlendSupportKind,
};
use crate::ids::{CurveId, SurfaceId};
use crate::math::Point3;
use crate::scalar::PositiveI64;
struct Fields {
leading_parameter: f64,
side_surface_range: [[Option<f64>; 2]; 2],
side_curve_range: [Option<f64>; 2],
side_location: [f64; 3],
center_range: [Option<f64>; 2],
radius: f64,
u_range: [Option<f64>; 2],
v_range: [Option<f64>; 2],
shape_parameter: f64,
shape_length: f64,
u_interval: [Option<f64>; 2],
discontinuity: f64,
}
const ADMITTED: Fields = Fields {
leading_parameter: 1.0,
side_surface_range: [[Some(0.0), None], [None, Some(4.0)]],
side_curve_range: [None, Some(6.0)],
side_location: [1.0, 2.0, 3.0],
center_range: [Some(0.0), Some(7.0)],
radius: 8.0,
u_range: [Some(9.0), None],
v_range: [None, Some(10.0)],
shape_parameter: 11.0,
shape_length: 12.0,
u_interval: [Some(13.0), None],
discontinuity: 14.0,
};
fn curve() -> CurveId {
CurveId::mint("synthetic:test:curve#g2").expect("valid identity")
}
fn surface() -> SurfaceId {
SurfaceId::mint("synthetic:test:surface#g2").expect("valid identity")
}
fn side(fields: &Fields) -> RollingBallSide {
RollingBallSide {
support_kind: VariableBlendSupportKind::Surface,
surface: Some(RollingBallSupportSurface {
surface: surface(),
parameter_ranges: fields.side_surface_range,
}),
curve: Some(RollingBallSupportCurve {
curve: curve(),
parameter_range: fields.side_curve_range,
}),
pcurve: None,
location: Point3::new(
fields.side_location[0],
fields.side_location[1],
fields.side_location[2],
),
secondary_pcurve: None,
extension: None,
}
}
fn stored(fields: &Fields) -> RevisionG2BlendConstructionWire {
RevisionG2BlendConstructionWire {
revision: PositiveI64::new(1).expect("positive revision"),
leading_parameters: [fields.leading_parameter, 2.0],
sides: Box::new([side(fields), side(&ADMITTED)]),
center: curve(),
center_range: fields.center_range,
radii: [fields.radius, 15.0],
radius_selector: RollingBallRadiusSelector::Value {
value: PositiveI64::new(3).expect("a positive selector"),
},
u_range: fields.u_range,
v_range: fields.v_range,
shape_prefix: 4,
shape_parameter: fields.shape_parameter,
shape_length: fields.shape_length,
shape_tail: 5,
cache: RevisionCacheForm::Parameterization(RevisionSurfaceParameterization {
u_interval: fields.u_interval,
v_interval: [None, None],
u_closure: 0,
v_closure: 0,
u_singularity: 0,
v_singularity: 0,
}),
discontinuities: [
vec![fields.discontinuity],
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
],
tail_flag: false,
tail_extensions: [6, 7, 8],
}
}
fn wire(fields: &Fields) -> Result<RevisionG2BlendConstruction, ProceduralGeometryError> {
RevisionG2BlendConstruction::try_from(stored(fields))
}
#[test]
fn the_revision_g2_blend_admission_writes_every_scalar_it_admits() {
let definition = ProceduralSurfaceDefinition::RevisionG2Blend {
construction: Box::new(
RevisionG2BlendConstruction::admit(stored(&ADMITTED)).expect("every scalar is finite"),
),
};
let value = serde_json::to_value(&definition).expect("serialize the definition");
let stored = &value["construction"];
assert_eq!(stored["leading_parameters"], serde_json::json!([1.0, 2.0]));
assert_eq!(
stored["sides"][0]["surface"]["parameter_ranges"],
serde_json::json!([[0.0, null], [null, 4.0]])
);
assert_eq!(
stored["sides"][0]["curve"]["parameter_range"],
serde_json::json!([null, 6.0])
);
assert_eq!(
stored["sides"][0]["location"],
serde_json::json!({"x": 1.0, "y": 2.0, "z": 3.0})
);
assert_eq!(stored["center_range"], serde_json::json!([0.0, 7.0]));
assert_eq!(stored["radii"], serde_json::json!([8.0, 15.0]));
assert_eq!(stored["u_range"], serde_json::json!([9.0, null]));
assert_eq!(stored["v_range"], serde_json::json!([null, 10.0]));
assert_eq!(stored["shape_parameter"], serde_json::json!(11.0));
assert_eq!(stored["shape_length"], serde_json::json!(12.0));
assert_eq!(
stored["cache"]["u_interval"],
serde_json::json!([13.0, null])
);
assert_eq!(stored["discontinuities"][0], serde_json::json!([14.0]));
assert_eq!(
serde_json::from_value::<ProceduralSurfaceDefinition>(value)
.expect("the definition round-trips"),
definition
);
assert!(wire(&ADMITTED).is_ok());
}
#[test]
fn the_revision_g2_blend_admits_the_full_positive_revision_lane() {
let definition = ProceduralSurfaceDefinition::RevisionG2Blend {
construction: Box::new(
RevisionG2BlendConstruction::admit(stored(&ADMITTED))
.expect("positive revision and finite scalars"),
),
};
let value = serde_json::to_value(definition).expect("serialize the definition");
for revision in [0_i64, -1] {
assert!(PositiveI64::new(revision).is_none());
let mut invalid = value.clone();
invalid["construction"]["revision"] = serde_json::json!(revision);
assert!(serde_json::from_value::<ProceduralSurfaceDefinition>(invalid).is_err());
}
let mut maximum = stored(&ADMITTED);
maximum.revision = PositiveI64::new(i64::MAX).expect("maximum is positive");
assert_eq!(
RevisionG2BlendConstruction::admit(maximum)
.expect("maximum revision is admitted")
.revision()
.get(),
i64::MAX
);
let mut maximum = value;
maximum["construction"]["revision"] = serde_json::json!(i64::MAX);
let parsed = serde_json::from_value::<ProceduralSurfaceDefinition>(maximum)
.expect("maximum JSON revision is admitted");
let ProceduralSurfaceDefinition::RevisionG2Blend { construction } = parsed else {
panic!("expected revision G2 blend")
};
assert_eq!(construction.revision().get(), i64::MAX);
}
#[test]
fn radius_selector_diagnostic_reports_the_rejected_integer() {
for value in [0, -2, i64::MIN] {
let error = serde_json::from_value::<PositiveI64>(serde_json::json!(value))
.unwrap_err()
.to_string();
assert!(error.contains(&value.to_string()), "{error}");
assert!(error.contains("positive"), "{error}");
}
}
#[test]
fn the_revision_g2_blend_admission_refuses_a_non_finite_scalar() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
for fields in [
Fields {
leading_parameter: value,
..ADMITTED
},
Fields {
side_surface_range: [[Some(0.0), None], [Some(value), None]],
..ADMITTED
},
Fields {
side_curve_range: [Some(value), None],
..ADMITTED
},
Fields {
side_location: [1.0, value, 3.0],
..ADMITTED
},
Fields {
center_range: [None, Some(value)],
..ADMITTED
},
Fields {
radius: value,
..ADMITTED
},
Fields {
u_range: [Some(value), None],
..ADMITTED
},
Fields {
v_range: [None, Some(value)],
..ADMITTED
},
Fields {
shape_parameter: value,
..ADMITTED
},
Fields {
shape_length: value,
..ADMITTED
},
Fields {
u_interval: [Some(value), None],
..ADMITTED
},
Fields {
discontinuity: value,
..ADMITTED
},
] {
assert!(RevisionG2BlendConstruction::admit(stored(&fields)).is_err());
assert!(wire(&fields).is_err());
}
}
}
#[test]
fn a_revision_g2_blend_holds_its_admitted_scalars() {
use crate::scalar::FiniteReal;
let construction =
RevisionG2BlendConstruction::admit(stored(&ADMITTED)).expect("every scalar is finite");
let finite = |value| FiniteReal::new(value).expect("finite fixture");
assert_eq!(
construction.leading_parameters(),
[finite(1.0), finite(2.0)]
);
assert_eq!(
construction.center_range(),
[Some(finite(0.0)), Some(finite(7.0))]
);
assert_eq!(construction.radii(), [finite(8.0), finite(15.0)]);
assert_eq!(construction.u_range(), [Some(finite(9.0)), None]);
assert_eq!(construction.v_range(), [None, Some(finite(10.0))]);
assert_eq!(construction.shape_parameter(), finite(11.0));
assert_eq!(construction.shape_length(), finite(12.0));
assert_eq!(construction.discontinuities()[0], vec![finite(14.0)]);
}