1use geop_core_editor::{
5 edit_form::{EditForm, FormState, choices, chosen},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Number, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_geometry::deform::AxialMap;
12use geop_core_math::{
13 geop_error::{GeopError, GeopResult},
14 scalars::Scalar,
15 vector::Vector3,
16};
17use geop_core_program::{part::StepContext, reader::Reader};
18use geop_core_topology::{Body, build::BodySpec};
19use geop_lib_booleans::{RemeshParams, imprint_on_face};
20use geop_ops_model::{
21 BodyNames, ModelOperation, NamedModel, Namer, build_model, read_model, resolve_bodies,
22};
23use serde::{Deserialize, Serialize};
24
25use crate::common::{BodyRef, bodies_field, body};
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum Deform {
31 #[default]
34 Taper,
35 Pinch,
37 Shear,
41 Twist,
44 Bend,
47}
48
49const DEFORMS: [(Deform, &str, &str); 5] = [
50 (Deform::Taper, "taper", "Taper"),
51 (Deform::Pinch, "pinch", "Pinch"),
52 (Deform::Shear, "shear", "Shear"),
53 (Deform::Twist, "twist", "Twist"),
54 (Deform::Bend, "bend", "Bend"),
55];
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
79pub struct DeformBody;
80
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82pub struct DeformBodyArgs {
83 pub bodies: Vec<BodyRef>,
85 #[serde(default)]
87 pub deform: Deform,
88 #[serde(default)]
90 pub origin: [f64; 3],
91 pub axis: [f64; 3],
93 #[serde(default)]
96 pub toward: [f64; 3],
97 pub start: f64,
99 pub end: f64,
100 pub amount: f64,
103}
104
105impl DeformBodyArgs {
106 fn map(&self) -> GeopResult<AxialMap> {
108 let numbers = (self.origin.iter())
109 .chain(&self.axis)
110 .chain(&self.toward)
111 .chain([&self.start, &self.end, &self.amount]);
112 if let Some(bad) = numbers.clone().find(|v| !v.is_finite()) {
113 return Err(GeopError::new(format!("{bad} is not a number")));
114 }
115 let point = |p: [f64; 3]| Vector3::from_array(p.map(Scalar::from_f64));
116 let (origin, axis) = (point(self.origin), point(self.axis));
117 let zone = (Scalar::from_f64(self.start), Scalar::from_f64(self.end));
118 let amount = Scalar::from_f64(self.amount);
119 match self.deform {
120 Deform::Taper => AxialMap::taper(origin, &axis, None, zone, amount),
121 Deform::Pinch => {
122 AxialMap::taper(origin, &axis, Some(&point(self.toward)), zone, amount)
123 }
124 Deform::Shear => AxialMap::shear(origin, &axis, &point(self.toward), zone, amount),
125 Deform::Twist => AxialMap::twist(origin, &axis, zone, amount.degrees_to_radians()?),
126 Deform::Bend => AxialMap::bend(
127 origin,
128 &axis,
129 &point(self.toward),
130 zone,
131 amount.degrees_to_radians()?,
132 ),
133 }
134 }
135}
136
137part_operation!(DeformBody(DeformBodyArgs));
138
139impl PartOperation for DeformBody {
140 const KIND: &'static str = "deform_body";
141 const INFO: OperationInfo = OperationInfo::new(
142 "Deform body",
143 "Taper, pinch, shear, twist or bend bodies about an axis, over a zone along it.",
144 OperationGroup::Bodies,
145 OperationTier::Menu,
146 )
147 .icon(crate::icons::DEFORM_BODY);
148 type Args = DeformBodyArgs;
149 type Session = ();
150
151 fn new_args(context: &Context) -> DeformBodyArgs {
154 let model = read_model(&context.before).ok();
155 let solid = model.as_ref().and_then(|m| m.newest_solid());
156 let (start, end) = (model.as_ref().zip(solid.as_ref()))
157 .and_then(|(m, s)| height(m, s))
158 .unwrap_or((0.0, 1.0));
159 DeformBodyArgs {
160 bodies: solid.map(body).into_iter().collect(),
161 deform: Deform::Taper,
162 origin: [0.0; 3],
163 axis: [0.0, 0.0, 1.0],
164 toward: [1.0, 0.0, 0.0],
165 start,
166 end,
167 amount: 0.5,
168 }
169 }
170
171 fn run(ctx: &mut StepContext, args: &DeformBodyArgs) -> GeopResult<()> {
172 let _step = ctx.step();
173 build_model::<Self>(ctx, args)
174 }
175
176 fn form<'a>(
179 context: &'a Context<'a>,
180 args: &DeformBodyArgs,
181 _: &FormState<()>,
182 ) -> EditForm<'a, DeformBodyArgs> {
183 let mut f = EditForm::<DeformBodyArgs>::default();
184 bodies_field(&mut f, &context.before, &args.bodies, |edit, picked| {
185 edit.args.bodies = picked
186 });
187 let (value, options) = choices(&DEFORMS, &args.deform);
188 f.select("deform", "deform", value, options, false, |edit, value| {
189 if let Some(deform) = chosen(&DEFORMS, value) {
190 edit.args.deform = deform;
191 }
192 });
193 let amount = match args.deform {
194 Deform::Taper | Deform::Pinch => Number::new("factor", args.amount, Unit::Fraction),
195 Deform::Shear => Number::new("offset", args.amount, Unit::Length),
196 Deform::Twist | Deform::Bend => Number::new("angle", args.amount, Unit::Angle),
197 };
198 f.number("amount", amount, |edit, v| edit.args.amount = v);
199 for (key, label, value) in [
200 ("start", "zone start", args.start),
201 ("end", "zone end", args.end),
202 ] {
203 let number = Number::new(label, value, Unit::Length);
204 f.number(key, number, move |edit, v| match key {
205 "start" => edit.args.start = v,
206 _ => edit.args.end = v,
207 });
208 }
209 type Field = fn(&mut DeformBodyArgs) -> &mut [f64; 3];
212 let mut vector =
213 |keys: [&'static str; 3], label: &str, value: [f64; 3], unit, field: Field| {
214 for (k, (key, axis)) in keys.into_iter().zip(["x", "y", "z"]).enumerate() {
215 let number = Number::new(format!("{label} {axis}"), value[k], unit);
216 f.number(key, number, move |edit, v| field(edit.args)[k] = v);
217 }
218 };
219 let origin = ["origin_x", "origin_y", "origin_z"];
220 vector(origin, "origin", args.origin, Unit::Length, |a| {
221 &mut a.origin
222 });
223 let axis = ["axis_x", "axis_y", "axis_z"];
224 vector(axis, "axis", args.axis, Unit::Fraction, |a| &mut a.axis);
225 if matches!(args.deform, Deform::Pinch | Deform::Shear | Deform::Bend) {
226 let toward = ["toward_x", "toward_y", "toward_z"];
227 vector(toward, "toward", args.toward, Unit::Fraction, |a| {
228 &mut a.toward
229 });
230 }
231 f
232 }
233}
234
235fn height(model: &NamedModel, solid: &str) -> Option<(f64, f64)> {
238 let id = model.solid_id(solid).ok()?;
239 let faces = model.topology().body_faces(id).ok()?;
240 let z = (faces.iter())
241 .map(|f| model.topology().faces[f].surface.aabb()[2])
242 .reduce(Scalar::union)?;
243 (z.lo.is_finite() && z.hi.is_finite() && z.hi > z.lo).then_some((z.lo, z.hi))
244}
245
246impl ModelOperation for DeformBody {
247 fn build(
248 model: &mut NamedModel,
249 reader: &Reader,
250 step: &str,
251 args: &DeformBodyArgs,
252 ) -> GeopResult<()> {
253 let map = args.map()?;
254 let namer = Namer::new(DeformBody::KIND, step)?;
255 let motion = map.as_motion()?;
256 for body in resolve_bodies(reader, &args.bodies)? {
257 if let Some(motion) = &motion {
258 model.transform_body(body, motion)?;
259 continue;
260 }
261 let (start, end) = map.zone();
262 for (side, t) in [("start", start), ("end", end)] {
263 divide_across(model, &namer.scoped(side), body, &map, t)?;
264 }
265 model.transform_body(body, &map)?;
266 }
267 Ok(())
268 }
269}
270
271fn divide_across(
277 model: &mut NamedModel,
278 namer: &Namer,
279 body: Body,
280 map: &AxialMap,
281 t: Scalar,
282) -> GeopResult<()> {
283 let topology = model.topology();
284 let mut vertices: Vec<_> = topology.iter_body_vertices(body)?.collect();
285 vertices.sort_by_key(|v| v.0);
286 let along = |v| map.along(&topology.vertices[&v].point);
287 let before = vertices.iter().any(|&v| along(v).definitely_less(t));
288 let after = vertices.iter().any(|&v| along(v).definitely_greater(t));
289 if !(before && after) {
290 return Ok(());
291 }
292 if let Some(&on) = vertices.iter().find(|&&v| along(v).could_be_equal(t)) {
293 return Err(GeopError::new(format!(
294 "the zone's end at {t:?} along the axis passes through the corner {} of the body; move the zone off it",
295 model.name_of(on).unwrap_or("?")
296 )));
297 }
298 let mut faces = topology.body_faces(body)?;
301 faces.sort_by_key(|f| f.0);
302 let crossing: Vec<_> = (faces.into_iter())
303 .filter(|&f| {
304 let corners: Vec<_> = (topology.face_edges(f))
305 .flat_map(|e| {
306 [
307 topology.edges[&e].start_vertex,
308 topology.edges[&e].end_vertex,
309 ]
310 })
311 .map(along)
312 .collect();
313 corners.iter().any(|c| c.definitely_less(t))
314 && corners.iter().any(|c| c.definitely_greater(t))
315 })
316 .collect();
317 let reach = (topology.body_faces(body)?.iter())
318 .map(|f| topology.faces[f].surface.size())
319 .try_fold(Scalar::ONE, |sum, s| Ok::<_, GeopError>(sum.add(s?)))?;
320 let reach = reach.add(
321 (vertices.iter())
322 .map(|&v| topology.vertices[&v].point.norm())
323 .fold(Scalar::ZERO, Scalar::max),
324 );
325 for face in crossing {
326 let name = model.name_of(face).unwrap_or("?").to_string();
327 let sheet_namer = namer.scoped(&name);
328 let spec = BodySpec::sheet_on(map.plane_across(t, reach.upper())?)?;
329 let names = BodyNames {
330 vertices: (0..4)
331 .map(|k| sheet_namer.name(&[&format!("v{k}")]))
332 .collect(),
333 edges: (0..4)
334 .map(|k| sheet_namer.name(&[&format!("e{k}")]))
335 .collect(),
336 faces: vec![sheet_namer.name(&["plane"])],
337 solid: None,
338 };
339 let built = model.build_body(spec, names)?;
340 let sheet = *built
341 .shells
342 .first()
343 .ok_or_else(|| GeopError::new("a sheet came out as no shell"))?;
344 imprint_on_face(
345 model,
346 &sheet_namer,
347 face,
348 &[Body::Sheet(sheet)],
349 RemeshParams::default(),
350 )
351 .map_err(|e| e.with_context(format!("dividing face {name} at the zone's end {t:?}")))?;
352 }
353 Ok(())
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use geop_lib_sweep::shapes::{block_part, cylinder};
360 use geop_ops_model::testing::{apply_to_model, assert_valid};
361
362 fn solid_volume(part: &NamedModel) -> f64 {
363 let solid = part.solid_id(&part.solid_names()[0]).unwrap();
364 (part.topology().mass_properties(solid).unwrap().volume).to_f64()
365 }
366
367 fn deformed(part: NamedModel, args: DeformBodyArgs) -> GeopResult<NamedModel> {
368 apply_to_model::<DeformBody>(part, "d", &args)
369 }
370
371 fn args(part: &NamedModel, deform: Deform, zone: (f64, f64), amount: f64) -> DeformBodyArgs {
372 DeformBodyArgs {
373 bodies: vec![body(part.solid_names()[0].clone())],
374 deform,
375 origin: [0.0; 3],
376 axis: [0.0, 0.0, 1.0],
377 toward: [1.0, 0.0, 0.0],
378 start: zone.0,
379 end: zone.1,
380 amount,
381 }
382 }
383
384 #[test]
387 fn a_box_tapered_to_half_is_a_frustum() {
388 let part = block_part("b", [-1.0, -1.0, 0.0], [1.0, 1.0, 3.0]);
389 let part = deformed(part.clone(), args(&part, Deform::Taper, (0.0, 3.0), 0.5)).unwrap();
390 assert_valid(&part);
391 let volume = 3.0 * (4.0 + 2.0 + 1.0) / 3.0;
392 assert!(
393 (solid_volume(&part) - volume).abs() < 1e-9,
394 "{}",
395 solid_volume(&part)
396 );
397 }
398
399 #[test]
402 fn a_twisted_box_keeps_its_volume() {
403 let part = block_part("b", [-1.0, -0.5, 0.0], [1.0, 0.5, 4.0]);
404 let part = deformed(part.clone(), args(&part, Deform::Twist, (0.0, 4.0), 90.0)).unwrap();
405 assert_valid(&part);
406 assert!(
407 (solid_volume(&part) - 8.0).abs() < 1e-6,
408 "{}",
409 solid_volume(&part)
410 );
411 }
412
413 #[test]
419 fn a_bar_bent_over_part_of_its_length() {
420 let part = block_part("b", [-0.5, -0.5, 0.0], [0.5, 0.5, 6.0]);
421 let a = args(&part, Deform::Bend, (2.0, 4.0), 90.0);
422 let part = deformed(part, a).unwrap();
423 assert_valid(&part);
424 let radius = 4.0 / std::f64::consts::PI;
425 let corner = [radius + 2.0, 0.5, 1.5 + radius];
426 let found = (part.topology().vertices.values())
427 .any(|v| (0..3).all(|k| (v.point[k].to_f64() - corner[k]).abs() < 1e-9));
428 assert!(found, "no corner at {corner:?}");
429 assert_eq!(part.topology().faces.len(), 14);
431 }
432
433 #[test]
436 fn a_cylinder_beside_the_axis_twisted_and_tapered() {
437 let mut part = NamedModel::new();
438 cylinder(&mut part, "c", [1.5, 0.0, 0.0], 0.5, 3.0);
439 let part = deformed(part.clone(), args(&part, Deform::Twist, (0.0, 3.0), 60.0)).unwrap();
440 assert_valid(&part);
441 let part = deformed(part.clone(), args(&part, Deform::Taper, (0.0, 3.0), 0.6)).unwrap();
442 assert_valid(&part);
443 }
444
445 #[test]
447 fn a_cylinder_twisted_about_its_axis() {
448 let mut part = NamedModel::new();
449 cylinder(&mut part, "c", [0.0; 3], 1.0, 2.0);
450 let part = deformed(part.clone(), args(&part, Deform::Twist, (0.0, 2.0), 45.0)).unwrap();
451 assert_valid(&part);
452 let volume = 2.0 * std::f64::consts::PI;
453 assert!(
454 (solid_volume(&part) - volume).abs() < 1e-6,
455 "{}",
456 solid_volume(&part)
457 );
458 }
459
460 #[test]
465 fn a_box_pinched_and_a_box_sheared() {
466 let part = block_part("b", [-1.0, -1.0, 0.0], [1.0, 1.0, 3.0]);
467 let part = deformed(part.clone(), args(&part, Deform::Pinch, (0.0, 3.0), 0.5)).unwrap();
468 assert_valid(&part);
469 let volume = 3.0 * 2.0 * (2.0 + 1.0) / 2.0;
470 assert!(
471 (solid_volume(&part) - volume).abs() < 1e-9,
472 "{}",
473 solid_volume(&part)
474 );
475
476 let part = block_part("b", [-1.0, -1.0, 0.0], [1.0, 1.0, 3.0]);
477 let mut sheared = args(&part, Deform::Shear, (0.0, 2.0), 1.0);
478 sheared.toward = [0.0, 1.0, 0.0];
479 let part = deformed(part, sheared).unwrap();
480 assert_valid(&part);
481 assert_eq!(part.topology().faces.len(), 6);
482 assert!((solid_volume(&part) - 12.0).abs() < 1e-9);
483 let top = [1.0, 2.5, 3.0];
484 let found = (part.topology().vertices.values())
485 .any(|v| (0..3).all(|k| (v.point[k].to_f64() - top[k]).abs() < 1e-12));
486 assert!(found, "no corner at {top:?}");
487 }
488
489 #[test]
491 fn refusals_say_why() {
492 let part = block_part("b", [0.0; 3], [1.0, 1.0, 2.0]);
493 let error =
494 deformed(part.clone(), args(&part, Deform::Taper, (0.0, 2.0), 0.0)).unwrap_err();
495 assert!(format!("{error}").contains("positive"), "{error}");
496 }
497}