Skip to main content

geop_ops_extrude_revolve/
cube.rs

1//! An axis-aligned box, built by [`extrude`]-ing its bottom face straight up
2//! — no vertex/edge is ever duplicated then welded, since `extrude` shares
3//! every side wall's edges with its top/bottom caps by construction (see its
4//! own module docs).
5
6use crate::{
7    common::{Profile, polygon},
8    extrude::{ExtrudeNames, extrude},
9};
10use geop_core_math::{
11    geop_error::GeopResult,
12    primitives::CoordinateSystem,
13    scalars::Scalar,
14    vector::{Vector2, Vector3},
15};
16use geop_core_part::{Namer, Part};
17use geop_core_topology::SolidId;
18
19/// A box solid spanning `[min, max]`, built as `extrude`'s start cap (the
20/// unit square in `(u, v)`, at `z = max.z`) swept down to `min.z`.
21///
22/// Named as the operation `cube(name)`, after that square: its corners
23/// `p0..p3` (counter-clockwise from `min` as seen from above) and sides
24/// `c0..c3`, see [`ExtrudeNames`].
25pub fn cube_solid<S: Scalar>(
26    part: &mut Part<S>,
27    name: &str,
28    min: Vector3<S>,
29    max: Vector3<S>,
30) -> GeopResult<SolidId> {
31    let (x0, y0, z0) = (min[0], min[1], max[2]);
32    let (x1, y1) = (max[0], max[1]);
33
34    // The coordinate system's `u`/`v` span the bottom footprint (scaled to
35    // the box's own `x`/`y` extents, so `outer` is the unit square) and `w`
36    // is the extrude direction.
37    let origin = Vector3::from_array([x0, y0, z0]);
38    let u = Vector3::from_array([x1.sub(x0), S::ZERO, S::ZERO]);
39    let v = Vector3::from_array([S::ZERO, y1.sub(y0), S::ZERO]);
40    let w = Vector3::from_array([S::ZERO, S::ZERO, min[2].sub(z0)]);
41    let coordinate_system = CoordinateSystem::try_new(origin, u, v, w)?;
42
43    // CCW in `(u, v)`, so the bottom cap's outward (downward) normal comes
44    // out right once `extrude` sweeps it upward.
45    let outer = [
46        Vector2::from_array([S::ZERO, S::ZERO]),
47        Vector2::from_array([S::ONE, S::ZERO]),
48        Vector2::from_array([S::ONE, S::ONE]),
49        Vector2::from_array([S::ZERO, S::ONE]),
50    ];
51
52    let namer = Namer::new("cube", name)?;
53    extrude(
54        part,
55        &ExtrudeNames::single(&namer),
56        &coordinate_system,
57        &Profile::closed(polygon(&outer)?),
58        &[],
59    )
60}
61
62#[cfg(test)]
63mod tests {
64    use super::cube_solid;
65    use geop_core_math::{for_all_scalars, scalars::Scalar, vector::Vector3};
66    use geop_core_part::Part;
67    use geop_core_topology::contains::shell::{PointClassification, shell_contains};
68
69    const MAX: usize = 200;
70    const EPS: f64 = 1e-3;
71    const SEED: u64 = 7;
72
73    fn check_cube_has_expected_entity_counts<S: Scalar>() {
74        let mut part = Part::<S>::new();
75        cube_solid(
76            &mut part,
77            "t1",
78            Vector3::from_array([S::ZERO; 3]),
79            Vector3::from_array([S::ONE; 3]),
80        )
81        .unwrap();
82        let model = part.topology();
83
84        // 6 faces, 8 shared corner vertices, 12 shared edges (each with
85        // exactly 2 coedges, one per adjoining face) — `extrude` shares
86        // vertices/edges between its side walls and caps directly, no
87        // welding needed.
88        assert_eq!(model.faces.len(), 6);
89        assert_eq!(model.vertices.len(), 8);
90        assert_eq!(model.edges.len(), 12);
91        assert_eq!(model.coedges.len(), 24);
92        assert_eq!(model.shells.len(), 1);
93        assert_eq!(model.solids.len(), 1);
94        for face in model.faces.values() {
95            assert!(face.holes.is_empty());
96        }
97        for edge_id in model.edges.keys() {
98            assert_eq!(model.coedges_of_edge(*edge_id).len(), 2);
99        }
100    }
101    #[test]
102    fn cube_has_expected_entity_counts() {
103        for_all_scalars!(check_cube_has_expected_entity_counts);
104    }
105
106    fn check_cube_center_is_inside<S: Scalar>() {
107        let mut part = Part::<S>::new();
108        let solid_id = cube_solid(
109            &mut part,
110            "t2",
111            Vector3::from_array([S::ZERO; 3]),
112            Vector3::from_array([S::ONE; 3]),
113        )
114        .unwrap();
115        let model = part.topology();
116        let shell_id = model.get_solid(solid_id).unwrap().shells[0];
117        let p = Vector3::from_array([S::from_f64(0.5); 3]);
118        assert_eq!(
119            shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
120            PointClassification::Inside
121        );
122    }
123    #[test]
124    fn cube_center_is_inside() {
125        for_all_scalars!(check_cube_center_is_inside);
126    }
127
128    fn check_cube_outside_point_is_outside<S: Scalar>() {
129        let mut part = Part::<S>::new();
130        let solid_id = cube_solid(
131            &mut part,
132            "t3",
133            Vector3::from_array([S::ZERO; 3]),
134            Vector3::from_array([S::ONE; 3]),
135        )
136        .unwrap();
137        let model = part.topology();
138        let shell_id = model.get_solid(solid_id).unwrap().shells[0];
139        let p = Vector3::from_array([S::from_f64(-5.0), S::from_f64(0.5), S::from_f64(0.5)]);
140        assert_eq!(
141            shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
142            PointClassification::Outside
143        );
144    }
145    #[test]
146    fn cube_outside_point_is_outside() {
147        for_all_scalars!(check_cube_outside_point_is_outside);
148    }
149
150    fn check_rasterize_topology_cube<S: Scalar>() {
151        let mut part = Part::<S>::new();
152        cube_solid(
153            &mut part,
154            "t4",
155            Vector3::from_array([S::ZERO; 3]),
156            Vector3::from_array([S::ONE; 3]),
157        )
158        .unwrap();
159        let model = part.topology();
160
161        let scene = geop_ops_rasterize::rasterize_topology(&model, 8).unwrap();
162        assert!(!scene.points.is_empty());
163        assert!(!scene.lines.is_empty());
164        assert!(!scene.triangles_transparent.is_empty());
165        assert!(!scene.labels.is_empty());
166
167        std::fs::create_dir_all("outputs").unwrap();
168        scene.save_to_file("outputs/cube_topology.html").unwrap();
169    }
170    #[test]
171    fn rasterize_topology_cube() {
172        for_all_scalars!(check_rasterize_topology_cube);
173    }
174}