1use crate::sweep::{Frame, Path, Span, SweepLoop, sweep_drawn};
5use geop_core_math::{
6 geop_error::{GeopError, GeopResult},
7 primitives::CoordinateSystem,
8 scalars::Scalar,
9};
10use geop_core_topology::build::BuiltBody;
11use geop_ops_model::{NamedModel, Namer};
12
13pub fn extrusion(plane: &CoordinateSystem, heights: &[Scalar]) -> GeopResult<Path> {
19 let n = heights.len();
20 let ways: Vec<Option<bool>> = heights
21 .windows(2)
22 .map(|h| h[1].sub(h[0]).definite_sign())
23 .collect();
24 if n < 2 || ways.iter().any(|way| way.is_none() || *way != ways[0]) {
25 return Err(GeopError::new(format!(
26 "extrusion: heights {heights:?} that do not each go further one way"
27 )));
28 }
29 let station = |h: Scalar| Frame {
30 origin: plane.origin().add(&plane.w().prod_scalar(h)),
31 e1: *plane.u(),
32 e2: *plane.v(),
33 };
34 let along = plane
35 .u()
36 .prod_cross(plane.v())
37 .prod_dot(plane.w())
38 .mul(heights[n - 1].sub(heights[0]));
39 let along_normal = along.definite_sign().ok_or_else(|| {
40 GeopError::new(format!(
41 "extrusion: which way {heights:?} go along the plane's normal could be either"
42 ))
43 })?;
44 Ok(Path {
45 stations: heights.iter().map(|&h| station(h)).collect(),
46 spans: vec![Span::Line; n - 1],
47 closed: false,
48 along_normal,
49 station_names: (0..n)
50 .map(|s| match s {
51 0 => "start".to_string(),
52 s if s + 1 == n => "end".to_string(),
53 s => format!("l{s}"),
54 })
55 .collect(),
56 span_names: if n == 2 {
57 vec![None]
58 } else {
59 (0..n - 1).map(|j| Some(format!("q{j}"))).collect()
60 },
61 })
62}
63
64pub fn extrude(
79 model: &mut NamedModel,
80 namer: &Namer,
81 solid: Option<&str>,
82 plane: &CoordinateSystem,
83 heights: &[Scalar],
84 loops: &[SweepLoop],
85) -> GeopResult<BuiltBody> {
86 let path = extrusion(plane, heights)?;
87 sweep_drawn(model, namer, &path, loops, solid)
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::common::polygon;
94 use crate::shapes::{circle, circle_at, v2};
95 use geop_core_geometry::nurb_curve::{NurbCurve2D, sqrt2_over_2};
96 use geop_core_math::vector::Vector3;
97 use geop_ops_model::Profile;
98 use geop_ops_model::testing::{assert_model_valid, assert_model_valid_sheets};
99
100 fn unit_square() -> Vec<NurbCurve2D> {
101 polygon(&[v2(0.0, 0.0), v2(1.0, 0.0), v2(1.0, 1.0), v2(0.0, 1.0)]).unwrap()
102 }
103
104 fn square_hole(x0: f64, y0: f64, size: f64) -> Vec<NurbCurve2D> {
109 polygon(&[
110 v2(x0, y0),
111 v2(x0, y0 + size),
112 v2(x0 + size, y0 + size),
113 v2(x0 + size, y0),
114 ])
115 .unwrap()
116 }
117
118 fn axis_aligned_coordinate_system(origin: Vector3) -> CoordinateSystem {
122 let u = Vector3::from_array([Scalar::ONE, Scalar::ZERO, Scalar::ZERO]);
123 let v = Vector3::from_array([Scalar::ZERO, Scalar::ONE, Scalar::ZERO]);
124 let w = Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::from_f64(-1.0)]);
125 CoordinateSystem::try_new(origin, u, v, w).unwrap()
126 }
127
128 fn extruded(
130 cs: &CoordinateSystem,
131 outer: Vec<NurbCurve2D>,
132 holes: Vec<Vec<NurbCurve2D>>,
133 ) -> NamedModel {
134 let mut model = NamedModel::new();
135 let namer = Namer::new("extrude", "e").unwrap();
136 let loops: Vec<_> = std::iter::once(Profile::closed(outer))
137 .chain(
138 holes
139 .into_iter()
140 .enumerate()
141 .map(|(k, h)| Profile::closed(h).with_prefix(&format!("h{k}"))),
142 )
143 .map(SweepLoop::plain)
144 .collect();
145 extrude(
146 &mut model,
147 &namer,
148 Some(&namer.root()),
149 cs,
150 &[Scalar::ZERO, Scalar::ONE],
151 &loops,
152 )
153 .unwrap();
154 model.check_names().unwrap();
155 model
156 }
157
158 #[test]
159 fn extruded_square_is_valid() {
160 let cs = axis_aligned_coordinate_system(Vector3::from_array([Scalar::ZERO; 3]));
161 let model = extruded(&cs, unit_square(), vec![]);
162 let topology = model.topology();
163 assert_model_valid(topology);
164 assert_eq!(topology.faces.len(), 6);
165 assert_eq!(topology.vertices.len(), 8);
166 assert_eq!(topology.edges.len(), 12);
167 }
168
169 #[test]
174 fn a_kinked_curve_is_swept_into_a_face_per_leg() {
175 let mut model = NamedModel::new();
176 let prism = crate::shapes::creased_prism(&mut model, "p");
177 assert_model_valid(model.topology());
178 let names: Vec<String> = (model.topology().solid_faces(prism).unwrap().iter())
179 .map(|&f| model.name(f).unwrap().to_string())
180 .collect();
181 assert_eq!(names.len(), 3 + 2, "{names:?}");
182 assert!(model.face_id("prism(p,c0)").is_ok() && model.face_id("prism(p,c0#k1)").is_ok());
183
184 let whole = NurbCurve2D::join(&crate::shapes::circle(1.0)).unwrap();
185 let (t0, t1) = whole.domain();
186 let (a, b) = whole.split(t0.add(t1).div(Scalar::TWO).unwrap()).unwrap();
187 let cut = SweepLoop::plain(Profile::closed(vec![a, b]))
188 .at_corners()
189 .unwrap();
190 assert_eq!(cut.profile.curves.len(), 2);
191 }
192
193 #[test]
194 fn extruded_square_with_hole_is_valid() {
195 let hole = square_hole(0.25, 0.25, 0.5);
196 let cs = axis_aligned_coordinate_system(Vector3::from_array([Scalar::ZERO; 3]));
197 let model = extruded(&cs, unit_square(), vec![hole]);
198 let topology = model.topology();
199 assert_model_valid(topology);
200 assert_eq!(topology.faces.len(), 6 + 4);
203 assert_eq!(topology.vertices.len(), 8 + 8);
204 }
205
206 #[test]
207 fn extrude_offsets_footprint() {
208 let origin = Vector3::from_array([
209 Scalar::from_f64(2.0),
210 Scalar::from_f64(-1.0),
211 Scalar::from_f64(0.5),
212 ]);
213 let cs = axis_aligned_coordinate_system(origin);
214 let model = extruded(&cs, unit_square(), vec![]);
215 let topology = model.topology();
216 assert_model_valid(topology);
217 for vertex in topology.vertices.values() {
221 assert!(
222 vertex.point[2].could_be_equal(Scalar::from_f64(0.5))
223 || vertex.point[2].could_be_equal(Scalar::from_f64(-0.5)),
224 "vertex at unexpected height: {:?}",
225 vertex.point
226 );
227 }
228 }
229
230 #[test]
233 fn extrude_profile_away_from_origin() {
234 let cs = axis_aligned_coordinate_system(Vector3::from_array([Scalar::ZERO; 3]));
235 let outer = polygon(&[v2(-3.0, 2.0), v2(-1.0, 2.0), v2(-2.0, 5.0)]).unwrap();
236 let model = extruded(&cs, outer, vec![]);
237 let topology = model.topology();
238 assert_model_valid(topology);
239 assert_eq!(topology.faces.len(), 5);
240 }
241
242 #[test]
244 fn extruded_ring_is_valid() {
245 let cs = axis_aligned_coordinate_system(Vector3::from_array([Scalar::ZERO; 3]));
246 let outer = circle(2.0);
247 let hole = circle_at(0.3, 0.0, 1.0, true);
248 let model = extruded(&cs, outer, vec![hole]);
249 let topology = model.topology();
250 assert_model_valid(topology);
251 assert_eq!(topology.faces.len(), 2 + 4 + 4);
252 }
253
254 #[test]
257 fn extrude_both_directions() {
258 let plane = CoordinateSystem::try_new(
259 Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::ONE]),
260 Vector3::from_array([Scalar::ONE, Scalar::ZERO, Scalar::ZERO]),
261 Vector3::from_array([Scalar::ZERO, Scalar::ONE, Scalar::ZERO]),
262 Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::ONE]),
263 )
264 .unwrap();
265 let mut outer = polygon(&[v2(0.0, 0.0), v2(1.0, 0.0), v2(1.0, 1.0)]).unwrap();
266 outer.push(
267 NurbCurve2D::arc(v2(1.0, 1.0), v2(0.0, 1.0), v2(0.0, 0.0), sqrt2_over_2()).unwrap(),
268 );
269 outer.remove(2);
270 let namer = Namer::new("extrude", "e").unwrap();
271 let outer = SweepLoop::plain(Profile::closed(outer));
272 for (distance, far) in [(0.5, 1.5), (-0.5, 0.5)] {
273 let mut model = NamedModel::new();
274 extrude(
275 &mut model,
276 &namer,
277 Some(&namer.root()),
278 &plane,
279 &[Scalar::ZERO, Scalar::from_f64(distance)],
280 std::slice::from_ref(&outer),
281 )
282 .unwrap();
283 model.check_names().unwrap();
284 let topology = model.topology();
285 assert_model_valid(topology);
286 assert!(
287 topology
288 .vertices
289 .values()
290 .any(|v| v.point[2].could_be_equal(Scalar::from_f64(far)))
291 );
292 let p0_start = model.vertex_id("extrude(e,p0,start)").unwrap();
296 let p0_end = model.vertex_id("extrude(e,p0,end)").unwrap();
297 let point = |v| topology.get_vertex(v).unwrap().point;
298 assert!(point(p0_start).could_be_equal(&Vector3::from_array([
299 Scalar::ZERO,
300 Scalar::ZERO,
301 Scalar::ONE
302 ])));
303 assert!(point(p0_end)[2].could_be_equal(Scalar::from_f64(far)));
304 let end_cap = model.face_id("extrude(e,end)").unwrap();
305 assert!(topology.iterate_face_coedges(end_cap).all(|c| {
306 topology.coedge_start_vertex(c).unwrap().point[2]
307 .could_be_equal(Scalar::from_f64(far))
308 }));
309 }
310 }
311
312 fn xy_plane() -> CoordinateSystem {
313 CoordinateSystem::world_at(Vector3::zero())
314 }
315
316 #[test]
319 fn open_chain_extrudes_into_a_sheet() {
320 let mut model = NamedModel::new();
321 let namer = Namer::new("extrude", "e").unwrap();
322 let chain = crate::common::polyline(&[v2(0.0, 0.0), v2(1.0, 0.0), v2(1.0, 2.0)]).unwrap();
323 let built = extrude(
324 &mut model,
325 &namer,
326 None,
327 &xy_plane(),
328 &[Scalar::ZERO, Scalar::ONE],
329 &[SweepLoop::plain(Profile::open(chain))],
330 )
331 .unwrap();
332 model.check_names().unwrap();
333 assert!(built.solid.is_none());
334 let topology = model.topology();
335 assert_model_valid_sheets(topology);
336 assert_eq!(topology.faces.len(), 2);
337 let free = |e: &geop_core_topology::EdgeId| topology.coedges_of_edge(*e).len() == 1;
338 assert_eq!(topology.edges.keys().filter(|e| free(e)).count(), 6);
339 assert!(model.edge_id("extrude(e,p1)").is_ok_and(|e| !free(&e)));
340 }
341
342 #[test]
344 fn extrude_between_two_ends() {
345 for (from, to) in [(-0.5, 1.0), (1.0, -0.5), (0.25, 2.0)] {
346 let mut model = NamedModel::new();
347 let namer = Namer::new("extrude", "e").unwrap();
348 extrude(
349 &mut model,
350 &namer,
351 Some("extrude(e)"),
352 &xy_plane(),
353 &[Scalar::from_f64(from), Scalar::from_f64(to)],
354 &[SweepLoop::plain(Profile::closed(unit_square()))],
355 )
356 .unwrap();
357 let topology = model.topology();
358 assert_model_valid(topology);
359 let height = |name: &str| {
360 topology
361 .get_vertex(model.vertex_id(name).unwrap())
362 .unwrap()
363 .point[2]
364 };
365 assert!(height("extrude(e,p0,start)").could_be_equal(Scalar::from_f64(from)));
366 assert!(height("extrude(e,p2,end)").could_be_equal(Scalar::from_f64(to)));
367 }
368 }
369}
370
371#[cfg(test)]
372mod naming_tests {
373 use super::*;
374 use crate::common::polygon;
375 use geop_core_math::scalars::Scalar;
376 use geop_core_math::vector::{Vector2, Vector3};
377 use geop_ops_model::Profile;
378
379 #[test]
384 fn extruded_square_names_follow_the_profile() {
385 let square = polygon(&[
386 Vector2::from_array([Scalar::ZERO, Scalar::ZERO]),
387 Vector2::from_array([Scalar::ONE, Scalar::ZERO]),
388 Vector2::from_array([Scalar::ONE, Scalar::ONE]),
389 Vector2::from_array([Scalar::ZERO, Scalar::ONE]),
390 ])
391 .unwrap();
392 let plane = CoordinateSystem::try_new(
393 Vector3::zero(),
394 Vector3::from_array([Scalar::ONE, Scalar::ZERO, Scalar::ZERO]),
395 Vector3::from_array([Scalar::ZERO, Scalar::ONE, Scalar::ZERO]),
396 Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::ONE]),
397 )
398 .unwrap();
399 let mut model = NamedModel::new();
400 let namer = Namer::new("extrude", "box").unwrap();
401 extrude(
402 &mut model,
403 &namer,
404 Some(&namer.root()),
405 &plane,
406 &[Scalar::ZERO, Scalar::ONE],
407 &[SweepLoop::plain(Profile::closed(square))],
408 )
409 .unwrap();
410 model.check_names().unwrap();
411
412 let description = geop_ops_model::PartDescription::of(&model).unwrap();
413 assert_eq!(description.solids.len(), 1);
414 assert_eq!(description.solids["extrude(box)"][0].len(), 6);
415 let mut side = description.faces["extrude(box,c0)"].outer.clone();
416 side.sort();
417 let mut expected: Vec<String> = [
418 "extrude(box,c0,end)",
419 "extrude(box,c0,start)",
420 "extrude(box,p0)",
421 "extrude(box,p1)",
422 ]
423 .iter()
424 .map(|e| e.to_string())
425 .collect();
426 expected.sort();
427 let unsigned: Vec<String> = side.iter().map(|e| e[1..].to_string()).collect();
428 let mut unsigned = unsigned;
429 unsigned.sort();
430 assert_eq!(unsigned, expected);
431 assert_eq!(
432 description.edges["extrude(box,p1)"].start,
433 "extrude(box,p1,start)"
434 );
435 assert_eq!(
436 description.edges["extrude(box,p1)"].end,
437 "extrude(box,p1,end)"
438 );
439 let corner = model.vertex_id("extrude(box,p2,end)").unwrap();
440 assert!(
441 model
442 .topology()
443 .get_vertex(corner)
444 .unwrap()
445 .point
446 .could_be_equal(&Vector3::from_array([Scalar::ONE; 3]))
447 );
448 }
449}