#![allow(clippy::unwrap_used)]
use std::io::Cursor;
use cadmpeg_ir::codec::{Codec, DecodeOptions};
use cadmpeg_ir::examples::unit_cube;
use cadmpeg_ir::geometry::SolvedSurfaceGeometry;
use cadmpeg_ir::math::{Point3, Vector3};
use cadmpeg_ir::CadIr;
use crate::test_support::exchange::export;
use crate::StepCodec;
fn planar_solid_volume_and_ccw_loops(ir: &CadIr) -> (f64, bool) {
use cadmpeg_ir::topology::{LoopBoundary, Sense};
let model = &ir.model;
let position = |vertex: &cadmpeg_ir::ids::VertexId| {
let vertex = model.vertices.iter().find(|v| &v.id == vertex).unwrap();
let point = model.points.iter().find(|p| p.id == vertex.point).unwrap();
point.position().get()
};
let origin = Point3::new(0.0, 0.0, 0.0);
let mut volume6 = 0.0;
let mut every_loop_ccw = true;
for face in &model.faces {
let surface = model
.surfaces
.iter()
.find(|s| s.id == face.surface)
.unwrap();
let Some(SolvedSurfaceGeometry::Plane(plane)) = surface.geometry.solved() else {
panic!("planar face");
};
let mut normal = *plane.frame().axis().as_raw();
if face.sense == Sense::Reversed {
normal = Vector3::new(-normal.x, -normal.y, -normal.z);
}
for loop_id in &face.loops {
let ring = model.loops.iter().find(|l| &l.id == loop_id).unwrap();
let LoopBoundary::Ring(ring) = &ring.boundary else {
panic!("edge loop");
};
let corners: Vec<Point3> = ring
.coedges()
.iter()
.map(|id| {
let coedge = model.coedges.iter().find(|c| &c.id == id).unwrap();
let edge = model.edges.iter().find(|e| e.id == coedge.edge).unwrap();
position(match coedge.sense {
Sense::Forward => &edge.start,
Sense::Reversed => &edge.end,
})
})
.collect();
let mut area_normal = Vector3::new(0.0, 0.0, 0.0);
for (index, a) in corners.iter().enumerate() {
let b = corners[(index + 1) % corners.len()];
let term = a.vector_from(origin).cross(b.vector_from(origin));
area_normal = Vector3::new(
area_normal.x + term.x,
area_normal.y + term.y,
area_normal.z + term.z,
);
}
every_loop_ccw &= area_normal.dot(normal) > 0.0;
for pair in corners[1..].windows(2) {
let [o, a, b] = [corners[0], pair[0], pair[1]].map(|p| p.vector_from(origin));
volume6 += o.dot(a.cross(b));
}
}
}
(volume6 / 6.0, every_loop_ccw)
}
#[test]
fn cube_round_trip_bounds_run_counterclockwise_about_outward_face_normals() {
const EPS_CUBE_VOLUME: f64 = 1.0e-9;
let source = unit_cube().expect("unit cube fixture is admitted");
let (volume, ccw) = planar_solid_volume_and_ccw_loops(&source);
assert!(ccw, "source cube loops run counterclockwise");
assert!((volume - 1000.0).abs() < EPS_CUBE_VOLUME, "{volume}");
let output = export(&source);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(output), &DecodeOptions::default())
.expect("decode written cube");
let (volume, ccw) = planar_solid_volume_and_ccw_loops(decoded.ir());
assert!(ccw, "written cube bounds run counterclockwise");
assert!((volume - 1000.0).abs() < EPS_CUBE_VOLUME, "{volume}");
}