use axiolid_model::{GeometryNode, SolidOperation};
use axiolid_primitive::Primitive;
use ifc_model::{EntityId, Model, Value};
use super::{lower_csg_primitive_node, lower_swept_disk_node};
use crate::lower::session::LoweringSession;
use crate::solid::testkit::{entity, n, r};
use crate::transform::Transform;
use crate::units::UnitScale;
fn millimetres() -> UnitScale {
UnitScale {
length_to_metres: 0.001,
angle_to_radians: 1.0,
}
}
fn point(x: f64, y: f64, z: f64) -> ifc_model::Entity {
entity(
"IFCCARTESIANPOINT",
vec![Value::List(vec![n(x), n(y), n(z)])],
)
}
fn placed_block() -> Model {
let mut model = Model::new();
model.insert(EntityId(1), point(1000.0, 2000.0, 0.0));
model.insert(
EntityId(2),
entity("IFCAXIS2PLACEMENT3D", vec![r(1), Value::Null, Value::Null]),
);
model.insert(
EntityId(3),
entity("IFCBLOCK", vec![r(2), n(1800.0), n(600.0), n(200.0)]),
);
model
}
#[test]
fn a_block_keeps_local_extents_and_carries_its_placement_separately() {
let model = placed_block();
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let node = lower_csg_primitive_node(&mut session, EntityId(3), Transform::identity())
.expect("the block must lower");
let lowered = session.finish(node).expect("finishes");
let instance = match lowered.graph.get(lowered.root).expect("root") {
GeometryNode::Instance(instance) => *instance,
other => panic!("expected an Instance, got {other:?}"),
};
match lowered.graph.get(instance.source).expect("source") {
GeometryNode::Primitive(Primitive::Block { x, y, z }) => {
assert!((x - 1.8).abs() < 1e-12, "1800 mm -> 1.8 m, got {x}");
assert!((y - 0.6).abs() < 1e-12, "600 mm -> 0.6 m, got {y}");
assert!((z - 0.2).abs() < 1e-12, "200 mm -> 0.2 m, got {z}");
}
other => panic!("expected a Block primitive, got {other:?}"),
}
let translation = instance.transform.translation.to_array();
let expected = [1.9, 2.3, 0.1];
for axis in 0..3 {
assert!(
(translation[axis] - expected[axis]).abs() < 1e-12,
"the placement must carry the converted corner, got {translation:?}"
);
}
}
fn swept_disk(inner: Option<f64>, start: Option<f64>, end: Option<f64>) -> Model {
let mut model = Model::new();
model.insert(EntityId(1), point(0.0, 0.0, 0.0));
model.insert(EntityId(2), point(3000.0, 0.0, 0.0));
model.insert(
EntityId(3),
entity("IFCPOLYLINE", vec![Value::List(vec![r(1), r(2)])]),
);
model.insert(
EntityId(4),
entity(
"IFCSWEPTDISKSOLID",
vec![
r(3),
n(50.0),
inner.map(n).unwrap_or(Value::Null),
start.map(n).unwrap_or(Value::Null),
end.map(n).unwrap_or(Value::Null),
],
),
);
model
}
#[test]
fn swept_disk_radii_convert_and_the_pipe_bore_survives() {
let model = swept_disk(Some(45.0), None, None);
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let node = lower_swept_disk_node(&mut session, EntityId(4), Transform::identity())
.expect("the swept disk must lower");
let lowered = session.finish(node).expect("finishes");
match lowered.graph.get(lowered.root).expect("root") {
GeometryNode::SolidOperation(SolidOperation::SweptDisk {
radius,
inner_radius,
..
}) => {
assert!(
(radius - 0.05).abs() < 1e-12,
"50 mm -> 0.05 m, got {radius}"
);
let inner = inner_radius.expect("the bore must survive: dropping it fills the pipe");
assert!(
(inner - 0.045).abs() < 1e-12,
"45 mm -> 0.045 m, got {inner}"
);
}
other => panic!("expected a SweptDisk, got {other:?}"),
}
}
#[test]
fn a_half_open_parameter_range_is_refused() {
let model = swept_disk(None, Some(0.0), None);
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let error = lower_swept_disk_node(&mut session, EntityId(4), Transform::identity())
.expect_err("one-sided trims must not be guessed at");
assert_eq!(error.entity(), Some(EntityId(4)));
}
#[test]
fn swept_disk_trim_parameters_follow_the_directrix_parameterisation() {
let model = swept_disk(None, Some(0.0), Some(2.0));
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let node =
lower_swept_disk_node(&mut session, EntityId(4), Transform::identity()).expect("lowers");
let lowered = session.finish(node).expect("finishes");
match lowered.graph.get(lowered.root).expect("root") {
GeometryNode::SolidOperation(SolidOperation::SweptDisk {
parameter_range, ..
}) => {
let (start, end) = parameter_range.expect("both ends present");
assert!((start - 0.0).abs() < 1e-12);
assert!(
(end - 2.0).abs() < 1e-12,
"a polyline parameter is an index and stays 2, got {end}"
);
}
other => panic!("expected a SweptDisk, got {other:?}"),
}
}
#[test]
fn an_inner_radius_not_smaller_than_the_outer_is_refused() {
let model = swept_disk(Some(50.0), None, None);
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let error = lower_swept_disk_node(&mut session, EntityId(4), Transform::identity())
.expect_err("inner >= outer has no volume and must be reported");
assert_eq!(error.entity(), Some(EntityId(4)));
}
#[test]
fn the_directrix_is_present_in_the_graph_as_a_curve() {
use axiolid_curve::Curve3;
let model = swept_disk(None, None, None);
let scale = millimetres();
let mut session = LoweringSession::new(&model, &scale);
let node =
lower_swept_disk_node(&mut session, EntityId(4), Transform::identity()).expect("lowers");
let lowered = session.finish(node).expect("finishes");
let directrix = match lowered.graph.get(lowered.root).expect("root") {
GeometryNode::SolidOperation(SolidOperation::SweptDisk { directrix, .. }) => *directrix,
other => panic!("expected a SweptDisk, got {other:?}"),
};
match lowered
.graph
.get(directrix)
.expect("the directrix node must exist")
{
GeometryNode::Curve3(Curve3::Polyline(pl)) => {
assert_eq!(pl.points.len(), 2, "both directrix points must survive");
}
other => panic!("expected a Polyline directrix, got {other:?}"),
}
}