1use geop_core_geometry::{
22 intersection::curve_surface_intersect,
23 nurb_curve::{NurbCurve2D, NurbCurve3D},
24};
25use geop_core_math::{
26 geop_error::{GeopError, GeopResult, WithContext},
27 plain,
28 primitives::CoordinateSystem,
29 scalars::Scalar,
30 vector::{Vector2, Vector3},
31 with_context,
32};
33use geop_core_topology::{
34 Body, SolidId,
35 contains::{
36 face::{PointClassification as OnFace, face_contains},
37 shell::{Classified, PointClassification, solid_contains},
38 },
39};
40use geop_lib_booleans::{Reach, combine_tools};
41use geop_lib_sweep::{SweepLoop, extrude};
42use geop_ops_model::{Combine, Profile, Tool, face_hull};
43use geop_ops_model::{NamedModel, Namer, ToolReach};
44
45use geop_core_geometry::search::{MAX_NODES, min_subdivision_size};
46
47use crate::band::{Chain, Piece};
48
49const SEED: u64 = 0x7269_6273;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum Growth {
56 Normal { flipped: bool },
58 Parallel { flipped: bool },
61}
62
63#[allow(clippy::too_many_arguments)]
70pub fn rib(
71 part: &mut NamedModel,
72 namer: &Namer,
73 operation_id: &str,
74 plane: &CoordinateSystem,
75 profile: &Profile,
76 target: SolidId,
77 (near, far): (Scalar, Scalar),
78 growth: Growth,
79) -> GeopResult<()> {
80 let ctx = with_context!(
81 "rib({}, target={target}, thickness=({near:?}, {far:?}), {growth:?})",
82 namer.root()
83 );
84 if !far.sub(near).definitely_greater(Scalar::ZERO) {
85 return Err(GeopError::new("a rib is thicker than zero")).with_context(ctx);
86 }
87 if profile.is_closed() {
88 return Err(GeopError::new(
89 "a rib grows from an open profile: its sketch encloses an area",
90 ))
91 .with_context(ctx);
92 }
93 let target_name = part.name(target).with_context(ctx)?.to_string();
94 let chain = extended(part, plane, profile, target).with_context(ctx)?;
95 let reach = reach_past(part, plane, target).with_context(ctx)?;
96 let tool_name = namer.name(&["tool"]);
97 let (built, start, end) = match growth {
98 Growth::Normal { flipped } => {
99 let loops: Vec<SweepLoop> = chain
100 .band(near, far)
101 .with_context(ctx)?
102 .into_iter()
103 .map(SweepLoop::plain)
104 .collect();
105 let to = if flipped { reach.neg() } else { reach };
106 let built = extrude(
107 part,
108 namer,
109 Some(&tool_name),
110 plane,
111 &[Scalar::ZERO, to],
112 &loops,
113 )
114 .with_context(ctx)?;
115 (built, namer.name(&["start"]), namer.name(&["end"]))
116 }
117 Growth::Parallel { flipped } => {
118 let swept = swept_region(&chain, reach, flipped).with_context(ctx)?;
119 let built = extrude(
120 part,
121 namer,
122 Some(&tool_name),
123 plane,
124 &[near, far],
125 &[SweepLoop::plain(swept)],
126 )
127 .with_context(ctx)?;
128 (
132 built,
133 namer.name(&[&profile.curve_names[0]]),
134 namer.name(&["far"]),
135 )
136 }
137 };
138 let tool = Tool {
139 solid: built.solid.expect("extruded as a solid"),
140 reach: ToolReach {
141 up_to_next: Some((start, end)),
142 scope: None,
143 },
144 };
145 let combine = Combine::join(target_name);
146 match combine_tools(part, namer, operation_id, &[tool], &combine).with_context(ctx)? {
147 Reach::Stopped(()) => Ok(()),
148 Reach::RunsOn(e) => Err(e.with_context(
149 "the rib's profile grows past the solid without meeting it all along: it has to run between faces that close it in",
150 ))
151 .with_context(ctx),
152 }
153}
154
155fn reach_past(part: &NamedModel, plane: &CoordinateSystem, target: SolidId) -> GeopResult<Scalar> {
159 let model = part.topology();
160 let hull = face_hull(model, &model.body_faces(Body::Solid(target))?)?;
161 let (lo, hi) = plain::bounds(hull.iter().map(|p| p.to_f64_array()))
162 .ok_or_else(|| GeopError::new(format!("solid {target:?} has no faces")))?;
163 let origin = [0, 1, 2].map(|k| plane.origin()[k].to_f64());
164 let diagonal = plain::diagonal(lo, hi);
165 let furthest = (0..8)
166 .map(|corner: usize| {
167 (0..3)
168 .map(|k| {
169 let x = if corner >> k & 1 == 1 { hi[k] } else { lo[k] };
170 (x - origin[k]).powi(2)
171 })
172 .sum::<f64>()
173 .sqrt()
174 })
175 .fold(0.0, f64::max);
176 Ok(Scalar::from_f64(furthest + diagonal))
177}
178
179fn extended(
184 part: &NamedModel,
185 plane: &CoordinateSystem,
186 profile: &Profile,
187 target: SolidId,
188) -> GeopResult<Chain> {
189 let lift = |p: &Vector2| plane.uv_to_xyz(p);
190 let mut pieces = Vec::with_capacity(profile.curves.len() + 2);
191 for (curve, name) in profile.curves.iter().zip(&profile.curve_names) {
192 pieces.push(Piece {
193 curve: curve.clone(),
194 center: arc_center(curve)?,
195 name: name.clone(),
196 });
197 }
198 let mut joints = profile.joint_names.clone();
199 let reach = reach_past(part, plane, target)?;
200 let first = &profile.curves[0];
201 let last = profile.curves.last().expect("a profile has curves");
202 let (t0, _) = first.domain();
203 let (_, t1) = last.domain();
204 let start = first.start()?;
205 let end = last.end()?;
206 let back = first.tangent(t0)?.normalize()?.neg();
207 let on = last.tangent(t1)?.normalize()?;
208 let into_wall = |from: Vector2, along: Vector2, joint: &str| {
209 wall_middle(
210 part,
211 target,
212 lift(&from),
213 &plane.uv_direction(&along),
214 reach,
215 )
216 .map_err(|e| e.with_context(format!("running the rib's end {joint} on")))
217 };
218 if let Some(at) = into_wall(start, back, &joints[0])? {
219 let to = plane.to_uv(&at);
220 pieces.insert(
221 0,
222 Piece {
223 curve: NurbCurve2D::line(to, start)?,
224 center: None,
225 name: "ext0".into(),
226 },
227 );
228 joints.insert(0, "end0".into());
229 }
230 let last_joint = joints.last().expect("joints").clone();
231 if let Some(at) = into_wall(end, on, &last_joint)? {
232 let to = plane.to_uv(&at);
233 pieces.push(Piece {
234 curve: NurbCurve2D::line(end, to)?,
235 center: None,
236 name: "ext1".into(),
237 });
238 joints.push("end1".into());
239 }
240 let n = pieces.len();
241 Ok(Chain {
242 pieces,
243 joints,
244 smooth: vec![false; n],
245 closed: false,
246 })
247}
248
249fn arc_center(curve: &NurbCurve2D) -> GeopResult<Option<Vector2>> {
251 if curve.degree == 1 {
252 return Ok(None);
253 }
254 let lifted = curve.lifted()?;
255 if lifted.as_line()?.is_some() {
256 return Ok(None);
257 }
258 let arc = lifted.as_arc()?.ok_or_else(|| {
259 GeopError::new("a rib's profile is made of lines and arcs, and this piece is neither")
260 })?;
261 Ok(Some(Vector2::from_array([
262 arc.circle.center[0],
263 arc.circle.center[1],
264 ])))
265}
266
267fn wall_middle(
273 part: &NamedModel,
274 target: SolidId,
275 from: Vector3,
276 along: &Vector3,
277 reach: Scalar,
278) -> GeopResult<Option<Vector3>> {
279 let model = part.topology();
280 let start = match solid_contains(
281 model,
282 target,
283 from,
284 MAX_NODES,
285 min_subdivision_size(),
286 SEED,
287 &mut Classified::default(),
288 )? {
289 PointClassification::Inside => return Ok(None),
290 PointClassification::Outside => None,
291 _ => Some(0.0),
292 };
293 let ray = NurbCurve3D::line(from, from.add(&along.prod_scalar(reach)))?;
294 let mut hits: Vec<Scalar> = Vec::new();
297 for face in model.body_faces(Body::Solid(target))? {
298 let surface = &model.get_face(face)?.surface;
299 let found =
300 curve_surface_intersect(&ray, surface, MAX_NODES, MAX_NODES, min_subdivision_size())?;
301 if found.is_coincident() {
302 return Err(GeopError::new(format!(
303 "it runs along face {} of the solid: draw the profile off the solid's faces",
304 part.name_of(face).unwrap_or("?")
305 )));
306 }
307 for (t, uv) in found.into_vec() {
308 if !t.definitely_greater(Scalar::ZERO) {
309 continue;
310 }
311 let class = face_contains(
312 model,
313 face,
314 uv[0].midpoint(),
315 uv[1].midpoint(),
316 MAX_NODES,
317 min_subdivision_size(),
318 SEED ^ face.0,
319 )?;
320 if class != OnFace::Outside && !hits.iter().any(|h| h.could_be_equal(t)) {
321 hits.push(t);
322 }
323 }
324 }
325 hits.sort_by(|a, b| a.to_f64().total_cmp(&b.to_f64()));
326 let (enter, leave) = match (start, hits.as_slice()) {
327 (Some(on), [leave, ..]) => (on, leave.to_f64()),
328 (None, [enter, leave, ..]) => (enter.to_f64(), leave.to_f64()),
329 _ => {
330 return Err(GeopError::new(
331 "it points at nothing of the solid: a rib's profile has to point at the walls it runs between",
332 ));
333 }
334 };
335 let t = Scalar::from_f64((enter + leave) / 2.0);
337 Ok(Some(from.add(&along.prod_scalar(reach.mul(t)))))
338}
339
340fn swept_region(chain: &Chain, reach: Scalar, flipped: bool) -> GeopResult<Profile> {
346 let first = &chain.pieces[0].curve;
347 let last = &chain.pieces.last().expect("pieces").curve;
348 let start = first.start()?;
349 let end = last.end()?;
350 let chord = end.sub(&start).normalize()?;
351 for piece in &chain.pieces {
352 let (t0, t1) = piece.curve.domain();
353 for t in [t0, t1] {
354 if !piece
355 .curve
356 .tangent(t)?
357 .prod_dot(&chord)
358 .definitely_greater(Scalar::ZERO)
359 {
360 return Err(GeopError::new(format!(
361 "{} turns back against the direction from the profile's start to its end: grown parallel to the sketch, the rib would fold over itself",
362 piece.name
363 )));
364 }
365 }
366 }
367 let left = chord.perp();
368 let sweep = if flipped { left.neg() } else { left }.prod_scalar(reach);
369 let (end_far, start_far) = (end.add(&sweep), start.add(&sweep));
370 let mut curves: Vec<NurbCurve2D> = chain.pieces.iter().map(|p| p.curve.clone()).collect();
371 curves.push(NurbCurve2D::line(end, end_far)?);
372 curves.push(NurbCurve2D::line(end_far, start_far)?);
373 curves.push(NurbCurve2D::line(start_far, start)?);
374 let mut curve_names: Vec<String> = chain.pieces.iter().map(|p| p.name.clone()).collect();
375 curve_names.extend(["side1".into(), "far".into(), "side0".into()]);
376 let mut joint_names = chain.joints.clone();
377 joint_names.extend(["far1".into(), "far0".into()]);
378 let region = Profile {
379 name: String::new(),
380 curves,
381 curve_names,
382 joint_names,
383 };
384 Ok(if flipped { region.reversed() } else { region })
387}