1use geop_core_program::{numeric::NumericInput, reader::Reader};
7use std::collections::HashSet;
8
9use geop_core_editor::{
10 edit_form::{Edit, EditForm},
11 editing::Number,
12};
13use geop_core_math::{
14 geop_error::{GeopError, GeopResult},
15 vector::Vector3,
16};
17use geop_core_sketch::{Shape, Sketch};
18use geop_core_topology::{SolidId, build::BuiltBody};
19use geop_lib_booleans::{Reach, RemeshParams, boolean, combine_tools, trim_up_to_next};
20use geop_ops_model::{BooleanOp, Combine, End, Extent, SolidRef, Tool, hull};
21use serde::{Deserialize, Serialize};
22
23use geop_ops_model::{NamedModel, Namer, ToolReach, read_model};
24
25use geop_lib_sweep::SweepLoop;
26
27pub use geop_ops_model::PathRef;
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum Output {
39 Solid(Combine),
40 Faces {
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 up_to: Option<SolidRef>,
43 },
44}
45
46impl Default for Output {
47 fn default() -> Self {
48 Output::Solid(Combine::NewBody)
49 }
50}
51
52impl From<Combine> for Output {
53 fn from(combine: Combine) -> Self {
54 Output::Solid(combine)
55 }
56}
57
58impl Output {
59 pub fn new_for(before: &NamedModel) -> Self {
62 Output::Solid(Combine::new_for(before))
63 }
64
65 pub fn shape(&self, sketch: &Sketch) -> GeopResult<Shape> {
69 match self {
70 Output::Faces { .. } => sketch.shape(),
71 Output::Solid(_) => sketch.regions().map(Shape::Regions),
72 }
73 }
74
75 fn stops(&self, model: &NamedModel) -> Vec<String> {
79 let target = match self {
80 Output::Solid(combine) => combine.target(),
81 Output::Faces { up_to } => up_to.as_ref().map(|solid| solid.name()),
82 };
83 match target {
84 Some(target) => vec![target.to_string()],
85 None => model.solid_names(),
86 }
87 }
88
89 pub(crate) fn fields<'a, A, T>(
96 &self,
97 f: &mut EditForm<'a, A, T>,
98 reader: &Reader<'a>,
99 reaches: bool,
100 set: impl Fn(&mut A, Output) + Clone + 'a,
101 ) {
102 let faces = matches!(self, Output::Faces { .. });
103 let turned = match self {
105 Output::Solid(Combine::With { target, .. }) => Output::Faces {
106 up_to: Some(target.clone()),
107 },
108 Output::Solid(Combine::NewBody) => Output::Faces { up_to: None },
109 Output::Faces {
110 up_to: Some(target),
111 } => Output::Solid(Combine::join(target.name())),
112 Output::Faces { up_to: None } => {
113 Output::new_for(&read_model(reader).unwrap_or_default())
114 }
115 };
116 let set_faces = set.clone();
117 f.checkbox("face", "face", faces, move |edit, checked| {
118 if checked != faces {
119 set_faces(edit.args, turned.clone());
120 }
121 });
122 match self {
123 Output::Solid(combine) => {
124 combine.fields(f, reader, move |args, combine| {
125 set(args, Output::Solid(combine))
126 });
127 }
128 Output::Faces { up_to } if reaches => {
129 Combine::target_field(
130 f,
131 reader,
132 "up to solid",
133 up_to.as_ref(),
134 move |edit, up_to| set(edit.args, Output::Faces { up_to: Some(up_to) }),
135 );
136 }
137 Output::Faces { .. } => {}
138 }
139 }
140
141 pub(crate) fn build_whole(
145 &self,
146 model: &mut NamedModel,
147 namer: &Namer,
148 step: &str,
149 sweep: impl FnOnce(&mut NamedModel, Option<&str>) -> GeopResult<BuiltBody>,
150 ) -> GeopResult<()> {
151 match self {
152 Output::Faces { .. } => sweep(model, None).map(drop),
153 Output::Solid(combine) => {
154 let built = sweep(model, Some(&combine.built_name(namer)))?;
155 let tool = Tool::whole(built.solid.expect("swept as a solid"));
156 combine_tools(model, namer, step, &[tool], combine)?.stopped()
157 }
158 }
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168pub struct Extents {
169 pub side1: Extent,
170 #[serde(default)]
171 pub symmetric: bool,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub side2: Option<Extent>,
174 #[serde(default)]
175 pub reversed: bool,
176}
177
178impl Extents {
179 pub fn blind(amount: impl Into<NumericInput>) -> Self {
181 Self {
182 side1: Extent::blind(amount),
183 symmetric: false,
184 side2: None,
185 reversed: false,
186 }
187 }
188
189 pub(crate) fn sweeps(&self, shape: &Shape) -> GeopResult<()> {
193 let next = |e: &Extent| matches!(e, Extent::UpToNext);
194 let up_to_next =
195 next(&self.side1) || (!self.symmetric && self.side2.as_ref().is_some_and(next));
196 match shape {
197 Shape::Regions(regions) if regions.len() > 1 && up_to_next => {
198 Err(GeopError::new(format!(
199 "the sketch has {} areas apart, and up to next goes as far as one area meets a face: sweep them blind or through all, or each up to next in a sketch of its own",
200 regions.len()
201 )))
202 }
203 _ => Ok(()),
204 }
205 }
206
207 pub fn reaches_target(&self) -> bool {
210 let far = |e: &Extent| matches!(e, Extent::UpToNext | Extent::ThroughAll);
211 far(&self.side1) || (!self.symmetric && self.side2.as_ref().is_some_and(far))
212 }
213
214 fn plan(
229 &self,
230 reader: &Reader,
231 far: impl Fn(f64) -> GeopResult<f64>,
232 full_turn: Option<f64>,
233 ) -> GeopResult<Plan> {
234 use End::*;
235 let ahead = if self.reversed { -1.0 } else { 1.0 };
236 let side1 = self.side1.end(reader)?;
237 let side2 = match &self.side2 {
238 Some(e) if !self.symmetric => Some(e.end(reader)?),
239 _ => None,
240 };
241 let side = |sign: f64, end: End| -> GeopResult<(Side, bool)> {
243 Ok(match end {
244 Blind(d) => (
245 Side {
246 to: sign * d,
247 up_to_next: false,
248 },
249 false,
250 ),
251 ThroughAll | UpToNext => (
252 Side {
253 to: sign * far(sign)?,
254 up_to_next: end == UpToNext,
255 },
256 true,
257 ),
258 })
259 };
260 let mut sides = match (side1, self.symmetric, side2) {
261 (Blind(d), true, _) => return Ok(Plan::Whole(-d / 2.0, d / 2.0)),
262 (e, true, _) => vec![side(ahead, e)?, side(-ahead, e)?],
263 (e, false, None) => vec![side(ahead, e)?],
264 (e, false, Some(e2)) => vec![side(ahead, e)?, side(-ahead, e2)?],
265 };
266 if let (Some(turn), [(a, far_a), (b, far_b)]) = (full_turn, sides.as_mut_slice())
267 && (a.up_to_next || b.up_to_next)
268 {
269 let overlap = a.to.abs() + b.to.abs() - turn;
270 let (give_a, give_b) = match (*far_a, *far_b) {
271 (true, true) => (overlap / 2.0, overlap / 2.0),
272 (true, false) => (overlap, 0.0),
273 (false, true) => (0.0, overlap),
274 (false, false) => (0.0, 0.0),
275 };
276 if overlap > 0.0 {
277 a.to -= a.to.signum() * give_a;
278 b.to -= b.to.signum() * give_b;
279 }
280 }
281 let sides: Vec<Side> = sides.into_iter().map(|(side, _)| side).collect();
282 Ok(if sides.iter().any(|s| s.up_to_next) {
283 Plan::Sides(sides)
284 } else {
285 Plan::Whole(sides.get(1).map_or(0.0, |s| s.to), sides[0].to)
286 })
287 }
288
289 pub(crate) fn fields<'a, A, T>(
299 &self,
300 f: &mut EditForm<'a, A, T>,
301 (key, default): (&str, f64),
302 mut amount: impl FnMut(&NumericInput, bool) -> Number,
303 get: impl Fn(&mut A) -> &mut Extents + Copy + 'a,
304 length: impl Fn(Edit<'_, A, T>, NumericInput) + 'a,
305 ) {
306 let side1 = Extent::mode(Some(&self.side1));
307 let choices = Extent::choices(false);
308 f.select("end", "end", side1, choices, false, move |edit, mode| {
309 let extents = get(edit.args);
310 if let Some(extent) = Extent::pick(Some(&extents.side1), mode, default) {
311 extents.side1 = extent;
312 }
313 });
314 if let Extent::Blind(d) = &self.side1 {
315 f.numeric(key, amount(d, false), length);
316 }
317 let reversed = self.reversed;
318 f.checkbox("reversed", "reverse direction", reversed, move |edit, b| {
319 get(edit.args).reversed = b
320 })
321 .checkbox("symmetric", "symmetric", self.symmetric, move |edit, b| {
322 get(edit.args).symmetric = b
323 });
324 if self.symmetric {
325 return;
326 }
327 let second = Extent::mode(self.side2.as_ref());
328 let choices = Extent::choices(true);
329 f.select(
330 "second",
331 "second side",
332 second,
333 choices,
334 false,
335 move |edit, mode| {
336 let extents = get(edit.args);
337 extents.side2 = Extent::pick(extents.side2.as_ref(), mode, default);
338 },
339 );
340 if let Some(Extent::Blind(d)) = &self.side2 {
341 f.numeric(&format!("{key}2"), amount(d, true), move |edit, length| {
342 get(edit.args).side2 = Some(Extent::Blind(length))
343 });
344 }
345 }
346}
347
348#[derive(Clone, Debug, PartialEq)]
351enum Plan {
352 Whole(f64, f64),
355 Sides(Vec<Side>),
357}
358
359#[derive(Clone, Copy, Debug, PartialEq)]
363struct Side {
364 to: f64,
365 up_to_next: bool,
366}
367
368fn side_namer(namer: &Namer, k: usize) -> Namer {
372 match k {
373 0 => namer.clone(),
374 k => namer.scoped(&format!("side{}", k + 1)),
375 }
376}
377
378pub(crate) trait Sweeping {
383 fn loops(&self) -> &[SweepLoop];
385
386 fn sweep(
390 &self,
391 model: &mut NamedModel,
392 namer: &Namer,
393 name: Option<&str>,
394 from: f64,
395 to: f64,
396 ) -> GeopResult<BuiltBody>;
397
398 fn past(&self, hull: &[Vector3], sign: f64) -> GeopResult<f64>;
402
403 fn ends(&self, to: f64) -> GeopResult<(String, String)>;
406
407 fn all_the_way(&self, far: f64) -> f64;
410
411 fn full_turn(&self) -> Option<f64>;
414
415 fn measure(&self, sign: f64, p: &Vector3) -> Option<f64>;
419}
420
421pub(crate) fn build_extents(
428 model: &mut NamedModel,
429 reader: &Reader,
430 namer: &Namer,
431 step: &str,
432 (extents, output): (&Extents, &Output),
433 sweeping: &impl Sweeping,
434) -> GeopResult<()> {
435 let stops = output.stops(model);
436 let hull = if extents.reaches_target() {
437 hull(model, &stops)?
438 } else {
439 None
440 };
441 let far = |sign| match &hull {
442 Some(hull) => sweeping.past(hull, sign),
443 None => Err(GeopError::new(
444 "up to next and through all go as far as a solid, and the model has none yet",
445 )),
446 };
447 let plan = extents.plan(reader, far, sweeping.full_turn())?;
448 match (output, plan) {
449 (_, Plan::Whole(from, to)) => output.build_whole(model, namer, step, |model, name| {
450 sweeping.sweep(model, namer, name, from, to)
451 }),
452 (Output::Faces { .. }, Plan::Sides(sides)) => {
453 for (k, side) in sides.into_iter().enumerate() {
454 let named = side_namer(namer, k);
455 let built = sweeping.sweep(model, &named, None, 0.0, side.to)?;
456 if side.up_to_next {
457 let (first, last) = sweeping.ends(side.to)?;
458 trim_side(
459 model,
460 step,
461 k,
462 &built,
463 &stops,
464 (&named, sweeping.loops()),
465 (&first, &last),
466 )?;
467 }
468 }
469 Ok(())
470 }
471 (Output::Solid(combine), Plan::Sides(sides)) => {
472 let mut tools = Vec::new();
473 for (k, &side) in sides.iter().enumerate() {
474 let named = side_namer(namer, k);
475 let name = combine.built_name(&named);
476 let built = sweeping.sweep(model, &named, Some(&name), 0.0, side.to)?;
477 tools.push(Tool {
480 solid: built.solid.expect("swept as a solid"),
481 reach: ToolReach {
482 up_to_next: side
483 .up_to_next
484 .then(|| (named.name(&["start"]), named.name(&["end"]))),
485 scope: (k > 0).then(|| format!("side{}", k + 1)),
486 },
487 });
488 }
489 combine_sides(model, namer, step, combine, tools, &sides, &stops, sweeping)
490 }
491 }
492}
493
494fn trim_side(
498 model: &mut NamedModel,
499 step: &str,
500 k: usize,
501 built: &BuiltBody,
502 targets: &[String],
503 (namer, loops): (&Namer, &[SweepLoop]),
504 (first, last): (&str, &str),
505) -> GeopResult<()> {
506 let targets = targets
507 .iter()
508 .map(|t| model.solid_id(t))
509 .collect::<GeopResult<Vec<_>>>()?;
510 let at = |station: &str| -> HashSet<String> {
511 loops
512 .iter()
513 .flat_map(|l| &l.profile.curve_names)
514 .map(|curve| namer.name(&[curve, station]))
515 .collect()
516 };
517 let (start, end) = (at(first), at(last));
518 let combine = side_namer(&Namer::new("combine", step)?, k);
519 for &sheet in &built.shells {
520 trim_up_to_next(
521 model,
522 &combine,
523 sheet,
524 &targets,
525 (&start, &end),
526 RemeshParams::default(),
527 )?;
528 }
529 Ok(())
530}
531
532#[allow(clippy::too_many_arguments)]
552fn combine_sides(
553 model: &mut NamedModel,
554 namer: &Namer,
555 step: &str,
556 combine: &Combine,
557 mut tools: Vec<Tool>,
558 sides: &[Side],
559 stops: &[String],
560 sweeping: &impl Sweeping,
561) -> GeopResult<()> {
562 let mut trial = model.clone();
563 let runs_on = match combine_tools(&mut trial, namer, step, &tools, combine)? {
564 Reach::Stopped(()) => {
565 *model = trial;
566 return Ok(());
567 }
568 Reach::RunsOn(error) => error,
569 };
570 let Combine::With { op, .. } = combine else {
571 return Err(runs_on);
572 };
573 let stops = stops
574 .iter()
575 .map(|s| model.solid_id(s))
576 .collect::<GeopResult<Vec<_>>>()?;
577 let rebuild = |model: &mut NamedModel, k: usize, from: f64, to: f64| {
579 let named = side_namer(namer, k);
580 let name = combine.built_name(&named);
581 let sign = sides[k].to.signum();
582 let built = sweeping.sweep(model, &named, Some(&name), from * sign, to * sign)?;
583 Ok::<_, GeopError>(built.solid.expect("swept as a solid"))
584 };
585 for k in 0..tools.len() {
586 let Some(caps) = tools[k].reach.up_to_next.take() else {
587 continue;
588 };
589 let solid = tools[k].solid;
590 let sign = sides[k].to.signum();
591 let measure = |p: &Vector3| sweeping.measure(sign, p);
592 let Some(contact) = first_contact(model, step, solid, &stops, measure)? else {
593 return Err(runs_on);
594 };
595 model.delete_bodies(&[solid.into()])?;
596 let far = sides[k].to.abs();
597 if *op != BooleanOp::Union {
598 let mut scratch = model.clone();
599 let before = rebuild(&mut scratch, k, 0.0, contact)?;
600 let mut from_outside = true;
601 for &stop in &stops {
602 from_outside &= overlap_with(&scratch, step, before, stop)?.is_none();
603 }
604 if from_outside {
605 let rest = Tool {
608 solid: rebuild(model, k, contact, far)?,
609 reach: ToolReach {
610 up_to_next: Some(caps),
611 scope: tools[k].reach.scope.clone(),
612 },
613 };
614 let mut trial = model.clone();
615 let alone = std::slice::from_ref(&rest);
616 if let Reach::Stopped(()) = combine_tools(&mut trial, namer, step, alone, combine)?
617 {
618 tools[k] = rest;
619 continue;
620 }
621 model.delete_bodies(&[rest.solid.into()])?;
622 }
623 }
624 tools[k].solid = rebuild(model, k, 0.0, sweeping.all_the_way(far))?;
625 }
626 combine_tools(model, namer, step, &tools, combine)?.stopped()
627}
628
629fn overlap_with(
632 model: &NamedModel,
633 step: &str,
634 tool: SolidId,
635 stop: SolidId,
636) -> GeopResult<Option<(NamedModel, SolidId)>> {
637 let mut scratch = model.clone();
638 let namer = Namer::new("contact", step)?;
639 let overlap = boolean(
640 &mut scratch,
641 &namer,
642 stop,
643 tool,
644 BooleanOp::Intersection,
645 RemeshParams::default(),
646 )?;
647 Ok(overlap.map(|overlap| (scratch, overlap)))
648}
649
650fn first_contact(
655 model: &NamedModel,
656 step: &str,
657 tool: SolidId,
658 stops: &[SolidId],
659 measure: impl Fn(&Vector3) -> Option<f64>,
660) -> GeopResult<Option<f64>> {
661 let mut least: Option<f64> = None;
662 for &stop in stops {
663 let Some((scratch, overlap)) = overlap_with(model, step, tool, stop)? else {
664 continue;
665 };
666 let topology = scratch.topology();
667 for vertex in topology.iter_body_vertices(overlap)? {
668 if let Some(at) = measure(&topology.get_vertex(vertex)?.point) {
669 least = Some(least.map_or(at, |l: f64| l.min(at)));
670 }
671 }
672 }
673 Ok(least)
674}