1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
15use ogeom_geom::{
16 BSplineSurface, CircleCurve, ConeSurface, Continuity, Curve, Curve2d as _, Curve3d as _,
17 CylinderSurface, Line2d, LineCurve, PlanarCurve, PlaneSurface, Surface as _, SurfaceGeometry,
18};
19use ogeom_math::{Axis, Axis2, Circle, Direction, Direction2, Frame, Point, Point2, Vector2};
20use ogeom_topo::{
21 EdgeRepr, FaceData, Location, Model, NodeData, Orientation, Shape, ShapeType, SurfaceId,
22};
23
24use crate::build::{attach_pcurve, edge_vertices, make_edge_between, make_wire};
25use crate::history::{Built, History};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Extension {
30 Natural,
34 Linear {
43 continuity: Continuity,
45 },
46}
47
48pub fn extend_face(
89 model: &mut Model,
90 shape: &Shape,
91 edge: &Shape,
92 length: f64,
93 mode: Extension,
94 tol: Tolerances,
95) -> OgeomResult<Built> {
96 if !(length > 0.0 && length.is_finite()) {
97 ogeom_bail!(
98 Construction,
99 "a face is extended by a positive length; got {length}"
100 );
101 }
102 let order = order_of(mode)?;
103 let kind = model.kind_of(shape)?;
104 if kind != ShapeType::Face {
105 ogeom_bail!(Construction, "extend_face extends a face; got a {kind:?}");
106 }
107 if model.kind_of(edge)? != ShapeType::Edge {
108 ogeom_bail!(Construction, "a face is extended across one of its edges");
109 }
110 let baked = crate::convert::baked_where_placed(model, shape, tol)?;
111 if !baked.history.is_empty() {
112 let Some(twin) = baked.history.modified(edge).first().cloned() else {
113 ogeom_bail!(Construction, "the edge is not on the face");
114 };
115 let grown = extend_face(model, &baked.shape, &twin, length, mode, tol)?;
116 return Ok(Built::new(grown.shape, baked.history.then(&grown.history)));
117 }
118 let (surface_id, face_tolerance) = {
119 let Some(NodeData::Face(data)) = model.node(shape).map(|n| n.data()) else {
120 ogeom_bail!(Dangling, "face is not in this model");
121 };
122 (data.surface, data.tolerance)
123 };
124
125 let wires = model.children_of(&shape.oriented(Orientation::Forward))?;
128 let Some(outer) = wires.first().cloned() else {
129 ogeom_bail!(
130 Construction,
131 "the face has no boundary, so no edge to extend it across"
132 );
133 };
134 let mut loops: Vec<Vec<Shape>> = Vec::with_capacity(wires.len());
135 for wire in &wires {
136 loops.push(model.children_of(&wire.oriented(Orientation::Forward))?);
137 }
138 let hits: Vec<usize> = loops[0]
139 .iter()
140 .enumerate()
141 .filter(|(_, e)| e.is_same(edge))
142 .map(|(i, _)| i)
143 .collect();
144 let at = match hits.as_slice() {
145 [i] => *i,
146 [] if loops[1..].iter().flatten().any(|e| e.is_same(edge)) => ogeom_bail!(
147 Construction,
148 "the edge bounds a hole in the face; a face is extended across its outer boundary"
149 ),
150 [] => ogeom_bail!(Construction, "the edge is not on the face"),
151 _ => ogeom_bail!(
152 Construction,
153 "the face's boundary runs along the edge twice (a seam); there is no one side \
154 to extend it across"
155 ),
156 };
157 let crossed = loops[0][at].clone();
158 let Some(NodeData::Edge(edge_data)) = model.node(&crossed).map(|n| n.data()) else {
159 ogeom_bail!(Dangling, "edge is not in this model");
160 };
161 if edge_data.degenerate {
162 ogeom_bail!(
163 Construction,
164 "a degenerate edge has no extent to extend the face across"
165 );
166 }
167 let Some((from_vertex, to_vertex)) = edge_vertices(model, &crossed)? else {
168 ogeom_bail!(Construction, "the edge has no vertices");
169 };
170 let Some(surface) = model.geometry().surface(surface_id).cloned() else {
171 ogeom_bail!(Dangling, "face refers to a surface not in this model");
172 };
173
174 let plan = match &surface {
175 SurfaceGeometry::Plane(plane) => plan_on_plane(
176 model,
177 *plane,
178 &loops[0],
179 &crossed,
180 &from_vertex,
181 &to_vertex,
182 length,
183 tol,
184 )?,
185 SurfaceGeometry::Cylinder(_)
186 | SurfaceGeometry::Cone(_)
187 | SurfaceGeometry::Sphere(_)
188 | SurfaceGeometry::Torus(_)
189 | SurfaceGeometry::BSpline(_) => plan_on_chart(
190 model, shape, &surface, surface_id, &crossed, length, order, tol,
191 )?,
192 other => ogeom_bail!(
193 Construction,
194 "a face on a {:?} surface is not extended; only planes, cylinders, cones, \
195 spheres, tori and B-spline patches are",
196 other.kind()
197 ),
198 };
199
200 let new_surface_id = match plan.surface {
201 Some(widened) => model.geometry_mut().add_surface(widened),
202 None => surface_id,
203 };
204 let Some(target) = model.geometry().surface(new_surface_id).cloned() else {
205 ogeom_bail!(Dangling, "the extended surface is not in this model");
206 };
207 if new_surface_id != surface_id {
208 let kept: Vec<Shape> = loops
209 .iter()
210 .flatten()
211 .filter(|e| !e.is_same(&crossed))
212 .cloned()
213 .collect();
214 retrim(model, &kept, surface_id, new_surface_id)?;
215 }
216
217 let far_from = model.add_point(target.point_at(plan.far.0.x, plan.far.0.y, tol)?);
218 let far_to = model.add_point(target.point_at(plan.far.1.x, plan.far.1.y, tol)?);
219 let side_a = chart_edge(
220 model,
221 &target,
222 new_surface_id,
223 plan.flat,
224 (plan.near.0, &from_vertex),
225 (plan.far.0, &far_from),
226 tol,
227 )?;
228 let far = chart_edge(
229 model,
230 &target,
231 new_surface_id,
232 plan.flat,
233 (plan.far.0, &far_from),
234 (plan.far.1, &far_to),
235 tol,
236 )?;
237 let side_b = chart_edge(
238 model,
239 &target,
240 new_surface_id,
241 plan.flat,
242 (plan.far.1, &far_to),
243 (plan.near.1, &to_vertex),
244 tol,
245 )?;
246
247 let mut ring = loops[0].clone();
248 ring.splice(at..=at, [side_a.clone(), far.clone(), side_b.clone()]);
249 let new_outer = make_wire(model, &ring, tol)?
250 .shape
251 .oriented(outer.orientation());
252 let mut new_wires = vec![new_outer];
253 new_wires.extend(wires[1..].iter().cloned());
254 let mut data = FaceData::new(new_surface_id, Location::identity());
255 data.tolerance = face_tolerance;
256 let face = model
257 .add_face(data, &new_wires)?
258 .oriented(shape.orientation());
259
260 let mut history = History::new();
261 history.modify(shape, face.clone());
262 history.modify(edge, far.oriented(Orientation::Forward));
263 history.generate(edge, side_a.oriented(Orientation::Forward));
264 history.generate(edge, side_b.oriented(Orientation::Forward));
265 Ok(Built::new(face, history))
266}
267
268fn order_of(mode: Extension) -> OgeomResult<Option<usize>> {
271 match mode {
272 Extension::Natural => Ok(None),
273 Extension::Linear { continuity } => match continuity {
274 Continuity::C0 | Continuity::G1 | Continuity::C1 => Ok(Some(1)),
275 Continuity::G2 | Continuity::C2 => Ok(Some(2)),
276 Continuity::CInfinity => ogeom_bail!(
277 Construction,
278 "a linear extension carries derivatives to the second order at most; \
279 a join smooth to every order is the natural extension"
280 ),
281 },
282 }
283}
284
285struct Plan {
287 surface: Option<SurfaceGeometry>,
289 flat: bool,
292 near: (Point2, Point2),
294 far: (Point2, Point2),
296}
297
298#[allow(clippy::too_many_arguments)]
301fn plan_on_plane(
302 model: &Model,
303 plane: PlaneSurface,
304 outer: &[Shape],
305 crossed: &Shape,
306 from_vertex: &Shape,
307 to_vertex: &Shape,
308 length: f64,
309 tol: Tolerances,
310) -> OgeomResult<Plan> {
311 let frame = plane.plane().frame();
312 let chart = |p: Point| frame.to_local(p).xy();
313 let a = vertex_point(model, from_vertex)?;
314 let b = vertex_point(model, to_vertex)?;
315 let chord = b - a;
316 let span = chord.magnitude();
317 if span <= tol.confusion() {
318 ogeom_bail!(
319 Construction,
320 "the edge closes on itself; there is no straight run to extend across"
321 );
322 }
323 for p in edge_samples(model, crossed, 8, tol)? {
324 let off = (p - a).cross(chord).magnitude() / span;
325 if off > tol.confusion() {
326 ogeom_bail!(
327 Construction,
328 "a planar face is extended across a straight edge; this one strays {off} \
329 from its chord"
330 );
331 }
332 }
333 let mut polygon: Vec<Point2> = Vec::new();
336 for e in outer {
337 let mut samples = edge_samples(model, e, 8, tol)?;
338 if e.orientation() == Orientation::Reversed {
339 samples.reverse();
340 }
341 samples.pop();
342 polygon.extend(samples.into_iter().map(chart));
343 }
344 let mut area = 0.0;
348 if let Some(&anchor) = polygon.first() {
349 for (i, p) in polygon.iter().enumerate() {
350 let q = polygon[(i + 1) % polygon.len()];
351 area += (*p - anchor).cross(q - anchor);
352 }
353 }
354 let (pa, pb) = (chart(a), chart(b));
355 let along = (pb - pa).normalized(tol)?;
356 let outward = if area > 0.0 {
358 -along.perpendicular()
359 } else {
360 along.perpendicular()
361 };
362 let (qa, qb) = (pa + outward * length, pb + outward * length);
363 let ((ua, ub), (va, vb)) = plane.domain();
364 let inside = |p: Point2| p.x >= ua && p.x <= ub && p.y >= va && p.y <= vb;
365 let surface = if inside(qa) && inside(qb) {
366 None
367 } else {
368 let lo = (ua.min(qa.x).min(qb.x), va.min(qa.y).min(qb.y));
369 let hi = (ub.max(qa.x).max(qb.x), vb.max(qa.y).max(qb.y));
370 Some(SurfaceGeometry::Plane(PlaneSurface::over(
371 plane.plane(),
372 (lo.0, hi.0),
373 (lo.1, hi.1),
374 )?))
375 };
376 Ok(Plan {
377 surface,
378 flat: true,
379 near: (pa, pb),
380 far: (qa, qb),
381 })
382}
383
384#[allow(clippy::too_many_arguments)]
387fn plan_on_chart(
388 model: &Model,
389 face: &Shape,
390 surface: &SurfaceGeometry,
391 surface_id: SurfaceId,
392 crossed: &Shape,
393 length: f64,
394 order: Option<usize>,
395 tol: Tolerances,
396) -> OgeomResult<Plan> {
397 let Some(NodeData::Edge(data)) = model.node(crossed).map(|n| n.data()) else {
398 ogeom_bail!(Dangling, "edge is not in this model");
399 };
400 let (pcurve, range) = match data.pcurve_for(surface_id, crossed.location()) {
401 Some(EdgeRepr::PCurve { curve, range, .. }) => (*curve, *range),
402 Some(EdgeRepr::Seam { .. }) => ogeom_bail!(
403 Construction,
404 "the edge is a seam of the surface; there is no one side to extend the face across"
405 ),
406 _ => ogeom_bail!(
407 Construction,
408 "the edge has no trim on the face's surface, so where it runs in the chart is unknown"
409 ),
410 };
411 let Some(pcurve) = model.geometry().pcurve(pcurve) else {
412 ogeom_bail!(Dangling, "pcurve is not in this model");
413 };
414 let samples: Vec<Point2> = (0..=8)
415 .map(|k| pcurve.point_at(range.0 + (range.1 - range.0) * f64::from(k) / 8.0, tol))
416 .collect::<OgeomResult<_>>()?;
417 let (first, last) = (samples[0], samples[8]);
418 let (near_from, near_to) = if crossed.orientation() == Orientation::Reversed {
419 (last, first)
420 } else {
421 (first, last)
422 };
423 let slack = tol.parametric();
424 let still = |pick: fn(Point2) -> f64| {
425 samples
426 .iter()
427 .all(|p| (pick(*p) - pick(first)).abs() <= slack)
428 };
429 let across_u = if still(|p| p.x) {
431 true
432 } else if still(|p| p.y) {
433 false
434 } else {
435 ogeom_bail!(
436 Construction,
437 "the edge does not run along a parameter line of the surface, so there is no \
438 crossing parameter to extend the face along"
439 );
440 };
441 let cross = |p: Point2| if across_u { p.x } else { p.y };
442 let along = |p: Point2| if across_u { p.y } else { p.x };
443 let place = |c: f64, a: f64| {
444 if across_u {
445 Point2::new(c, a)
446 } else {
447 Point2::new(a, c)
448 }
449 };
450 let c = cross(first);
451
452 let Some(sides) = crate::mass_chart::material_sides(model, face, tol) else {
454 ogeom_bail!(
455 Construction,
456 "the face's boundary cannot be walked in its chart, so its side of the edge is unknown"
457 );
458 };
459 let mut inward = 0.0;
460 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
461 for (e, p, d) in &sides {
462 lo = lo.min(cross(*p));
463 hi = hi.max(cross(*p));
464 if e.is_same(crossed) {
465 inward += cross(Point2::from_vector(*d));
466 }
467 }
468 if inward == 0.0 {
469 ogeom_bail!(
470 Construction,
471 "the face's side of the edge cannot be told from its boundary walk"
472 );
473 }
474 let outward = -inward.signum();
475 let middle = 0.5 * (along(near_from) + along(near_to));
476
477 let periodic = if across_u {
478 surface.is_periodic_u()
479 } else {
480 surface.is_periodic_v()
481 };
482 let ((ua, ub), (va, vb)) = surface.domain();
483 let (dlo, dhi) = if across_u { (ua, ub) } else { (va, vb) };
484
485 let (target, widened) = if let SurfaceGeometry::BSpline(patch) = surface {
486 let (to, longer) = continue_patch(
487 patch,
488 across_u,
489 outward > 0.0,
490 c,
491 middle,
492 (dlo, dhi),
493 length,
494 order,
495 tol,
496 )?;
497 (to, Some(SurfaceGeometry::BSpline(longer)))
498 } else {
499 let straight = matches!(
500 surface,
501 SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_)
502 ) && !across_u;
503 if order.is_some() && !straight {
504 ogeom_bail!(
505 Construction,
506 "a linear extension of an analytic surface across a curved parameter line \
507 would leave the surface; extend it naturally"
508 );
509 }
510 let (du, dv) = surface.d1_at(place(c, middle).x, place(c, middle).y, tol)?;
511 let speed = if across_u { du } else { dv }.magnitude();
512 if speed <= tol.confusion() {
513 ogeom_bail!(
514 Construction,
515 "the surface stands still across the edge; there is no direction to extend in"
516 );
517 }
518 let step = length / speed;
519 let to = c + outward * step;
520 if periodic {
521 let period = dhi - dlo;
522 if hi - lo + step >= period - slack {
523 ogeom_bail!(
524 Construction,
525 "the extension would wrap the face round the periodic surface onto itself"
526 );
527 }
528 }
529 (to, widened_analytic(surface, across_u, c, to, slack)?)
530 };
531 Ok(Plan {
532 surface: widened,
533 flat: false,
534 near: (near_from, near_to),
535 far: (
536 place(target, along(near_from)),
537 place(target, along(near_to)),
538 ),
539 })
540}
541
542fn widened_analytic(
546 surface: &SurfaceGeometry,
547 across_u: bool,
548 from: f64,
549 to: f64,
550 slack: f64,
551) -> OgeomResult<Option<SurfaceGeometry>> {
552 if across_u {
553 return Ok(None);
555 }
556 let ((_, _), (va, vb)) = surface.domain();
557 let wider = (va.min(to), vb.max(to));
558 let holds = to >= va && to <= vb;
559 match surface {
560 SurfaceGeometry::Cylinder(c) => Ok((!holds).then_some(SurfaceGeometry::Cylinder(
561 CylinderSurface::new(c.cylinder(), wider)?,
562 ))),
563 SurfaceGeometry::Cone(c) => {
564 let apex = c.apex_height();
565 if (to - apex) * (from - apex) <= 0.0 || (to - apex).abs() <= slack {
566 ogeom_bail!(
567 Construction,
568 "the extension would reach the cone's apex, where the far edge collapses"
569 );
570 }
571 Ok((!holds).then_some(SurfaceGeometry::Cone(ConeSurface::new(c.cone(), wider)?)))
572 }
573 SurfaceGeometry::Sphere(_) => {
574 if to.abs() >= std::f64::consts::FRAC_PI_2 - slack {
575 ogeom_bail!(
576 Construction,
577 "the extension would reach the sphere's pole, where the far edge collapses"
578 );
579 }
580 Ok(None)
581 }
582 SurfaceGeometry::Torus(_) => Ok(None),
583 _ => ogeom_bail!(Construction, "not an analytic surface"),
584 }
585}
586
587#[allow(clippy::too_many_arguments)]
591fn continue_patch(
592 patch: &BSplineSurface,
593 across_u: bool,
594 at_end: bool,
595 c: f64,
596 middle: f64,
597 (dlo, dhi): (f64, f64),
598 length: f64,
599 order: Option<usize>,
600 tol: Tolerances,
601) -> OgeomResult<(f64, BSplineSurface)> {
602 let slack = tol.parametric();
603 let side = if at_end { dhi } else { dlo };
604 if (c - side).abs() > slack {
605 ogeom_bail!(
606 Construction,
607 "the edge is not on a side of the patch's domain; a B-spline face is extended \
608 across a side of its patch"
609 );
610 }
611 let closed = if across_u {
612 patch.is_closed_u(tol)
613 } else {
614 patch.is_closed_v(tol)
615 };
616 if closed {
617 ogeom_bail!(
618 Construction,
619 "the patch closes on itself across the edge; continuing it would overlap the face"
620 );
621 }
622 let degree = if across_u {
623 patch.u_knots().degree()
624 } else {
625 patch.v_knots().degree()
626 };
627 let order = order.unwrap_or(degree);
628 let mut reach = length;
631 for _ in 0..12 {
632 let longer = patch.extended(across_u, at_end, reach, order, tol)?;
633 let ((ua, ub), (va, vb)) = longer.domain();
634 let (lo, hi) = if across_u { (ua, ub) } else { (va, vb) };
635 let line = Curve::BSpline(if across_u {
636 longer.iso_v_curve(middle, tol)?
637 } else {
638 longer.iso_u_curve(middle, tol)?
639 });
640 let run = if at_end { (c, hi) } else { (lo, c) };
641 let available = crate::length::curve_length(&line, run, tol)?;
642 if available >= length {
643 let to = if at_end {
644 crate::length::parameter_at_length(&line, run, length, tol)?
645 } else {
646 crate::length::parameter_at_length(&line, run, available - length, tol)?
647 };
648 let (u, v) = match (across_u, at_end) {
649 (true, true) => ((ua, to), (va, vb)),
650 (true, false) => ((to, ub), (va, vb)),
651 (false, true) => ((ua, ub), (va, to)),
652 (false, false) => ((ua, ub), (to, vb)),
653 };
654 return Ok((to, longer.segment(u, v, tol)?));
655 }
656 reach *= 2.0;
657 }
658 ogeom_bail!(
659 Construction,
660 "the patch's continuation does not reach the length along the crossing line"
661 )
662}
663
664fn chart_edge(
669 model: &mut Model,
670 surface: &SurfaceGeometry,
671 surface_id: SurfaceId,
672 flat: bool,
673 (from, from_vertex): (Point2, &Shape),
674 (to, to_vertex): (Point2, &Shape),
675 tol: Tolerances,
676) -> OgeomResult<Shape> {
677 let (curve, range, pcurve, reversed) = if flat {
678 let a = surface.point_at(from.x, from.y, tol)?;
679 let b = surface.point_at(to.x, to.y, tol)?;
680 let run = from.distance(to);
681 let curve: Curve = LineCurve::new(Axis {
682 location: a,
683 direction: Direction::new(b - a, tol)?,
684 })
685 .into();
686 let pcurve: PlanarCurve = Line2d::over(
687 Axis2 {
688 location: from,
689 direction: Direction2::new(to - from, tol)?,
690 },
691 0.0,
692 run,
693 )?
694 .into();
695 (curve, (0.0, run), pcurve, false)
696 } else {
697 let fixed_u = (from.x - to.x).abs() <= tol.parametric();
698 let (fixed, a, b) = if fixed_u {
699 (from.x, from.y, to.y)
700 } else {
701 (from.y, from.x, to.x)
702 };
703 let (lo, hi) = (a.min(b), a.max(b));
704 let (curve, range, pcurve) = parameter_line(surface, fixed_u, fixed, lo, hi, tol)?;
705 (curve, range, pcurve, a > b)
706 };
707 for k in 0..=4 {
710 let t = range.0 + (range.1 - range.0) * f64::from(k) / 4.0;
711 let on_curve = curve.point_at(t, tol)?;
712 let uv = pcurve.point_at(t, tol)?;
713 let on_surface = surface.point_at(uv.x, uv.y, tol)?;
714 let off = on_curve.distance(on_surface);
715 if off > tol.confusion() {
716 ogeom_bail!(
717 Invariant,
718 "a new edge strays {off} from the surface it was built on"
719 );
720 }
721 }
722 let (start, end) = if reversed {
723 (to_vertex, from_vertex)
724 } else {
725 (from_vertex, to_vertex)
726 };
727 let edge = make_edge_between(model, curve, range, start, end, tol)?.shape;
728 attach_pcurve(
729 model,
730 &edge,
731 pcurve,
732 surface_id,
733 Location::identity(),
734 range,
735 )?;
736 if let Some(NodeData::Edge(data)) = model.node_mut(&edge).map(|n| n.data_mut()) {
737 data.assert_same_parameter(true);
738 }
739 Ok(if reversed { edge.reversed() } else { edge })
740}
741
742fn parameter_line(
746 surface: &SurfaceGeometry,
747 fixed_u: bool,
748 fixed: f64,
749 lo: f64,
750 hi: f64,
751 tol: Tolerances,
752) -> OgeomResult<(Curve, (f64, f64), PlanarCurve)> {
753 let place = |p: f64| {
754 if fixed_u {
755 Point2::new(fixed, p)
756 } else {
757 Point2::new(p, fixed)
758 }
759 };
760 let heading = if fixed_u {
761 Direction2::new(Vector2::new(0.0, 1.0), tol)?
762 } else {
763 Direction2::new(Vector2::new(1.0, 0.0), tol)?
764 };
765 if let SurfaceGeometry::BSpline(patch) = surface {
766 let curve = Curve::BSpline(if fixed_u {
768 patch.iso_u_curve(fixed, tol)?
769 } else {
770 patch.iso_v_curve(fixed, tol)?
771 });
772 let pcurve: PlanarCurve = Line2d::over(
773 Axis2 {
774 location: place(0.0),
775 direction: heading,
776 },
777 lo,
778 hi,
779 )?
780 .into();
781 return Ok((curve, (lo, hi), pcurve));
782 }
783 let start_uv = place(lo);
784 let start = surface.point_at(start_uv.x, start_uv.y, tol)?;
785 let (du, dv) = surface.d1_at(start_uv.x, start_uv.y, tol)?;
786 let tangent = if fixed_u { dv } else { du };
787 let speed = tangent.magnitude();
788 let ruling = matches!(
789 surface,
790 SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_)
791 ) && fixed_u;
792 let (curve, rate): (Curve, f64) = if ruling {
794 let line = LineCurve::new(Axis {
795 location: start,
796 direction: Direction::new(tangent, tol)?,
797 });
798 (line.into(), speed)
799 } else {
800 let centre = match surface {
801 SurfaceGeometry::Sphere(s) if fixed_u => s.sphere().centre(),
802 SurfaceGeometry::Torus(t) if fixed_u => {
803 let torus = t.torus();
804 let f = torus.frame();
805 let out = f.x().vector() * fixed.cos() + f.y().vector() * fixed.sin();
806 f.origin() + out * torus.major_radius()
807 }
808 _ => {
809 let f = axis_frame(surface)?;
811 let z = f.z().vector();
812 f.origin() + z * (start - f.origin()).dot(z)
813 }
814 };
815 let radial = start - centre;
816 let radius = radial.magnitude();
817 if radius <= tol.confusion() {
818 ogeom_bail!(
819 Construction,
820 "the parameter line collapses to a point; no edge runs along it"
821 );
822 }
823 let frame = Frame::new(
824 centre,
825 Direction::from_cross(radial, tangent, tol)?,
826 Direction::new(radial, tol)?,
827 tol,
828 )?;
829 let circle = CircleCurve::new(Circle::new(frame, radius, tol)?);
830 (circle.into(), speed / radius)
831 };
832 let end = rate * (hi - lo);
833 let pcurve: PlanarCurve = if (rate - 1.0).abs() <= f64::EPSILON * 16.0 {
834 Line2d::over(
835 Axis2 {
836 location: start_uv,
837 direction: heading,
838 },
839 0.0,
840 end,
841 )?
842 .into()
843 } else {
844 let knots = ogeom_math::KnotVector::new(vec![0.0, 0.0, end, end], 1)?;
845 ogeom_geom::BSpline2d::new(knots, vec![start_uv, place(hi)], tol)?.into()
846 };
847 Ok((curve, (0.0, end), pcurve))
848}
849
850fn axis_frame(surface: &SurfaceGeometry) -> OgeomResult<Frame> {
852 Ok(match surface {
853 SurfaceGeometry::Cylinder(c) => c.cylinder().frame(),
854 SurfaceGeometry::Cone(c) => c.cone().frame(),
855 SurfaceGeometry::Sphere(s) => s.sphere().frame(),
856 SurfaceGeometry::Torus(t) => t.torus().frame(),
857 _ => ogeom_bail!(Construction, "the surface has no axis"),
858 })
859}
860
861fn retrim(model: &mut Model, edges: &[Shape], from: SurfaceId, to: SurfaceId) -> OgeomResult<()> {
865 let mut done: Vec<Shape> = Vec::new();
866 for edge in edges {
867 if done.iter().any(|d| d.node() == edge.node()) {
868 continue;
869 }
870 done.push(edge.clone());
871 let Some(NodeData::Edge(data)) = model.node_mut(edge).map(|n| n.data_mut()) else {
872 ogeom_bail!(Dangling, "edge is not in this model");
873 };
874 let agreed = data.same_parameter();
875 let copies: Vec<EdgeRepr> = data
876 .representations
877 .iter()
878 .filter_map(|r| match r {
879 EdgeRepr::PCurve {
880 curve,
881 surface,
882 location,
883 range,
884 } if *surface == from => Some(EdgeRepr::PCurve {
885 curve: *curve,
886 surface: to,
887 location: location.clone(),
888 range: *range,
889 }),
890 EdgeRepr::Seam {
891 forward,
892 reversed,
893 surface,
894 location,
895 range,
896 } if *surface == from => Some(EdgeRepr::Seam {
897 forward: *forward,
898 reversed: *reversed,
899 surface: to,
900 location: location.clone(),
901 range: *range,
902 }),
903 _ => None,
904 })
905 .collect();
906 for repr in copies {
907 data.add(repr);
908 }
909 data.assert_same_parameter(agreed);
910 }
911 Ok(())
912}
913
914fn vertex_point(model: &Model, vertex: &Shape) -> OgeomResult<Point> {
916 let Some(data) = model.node(vertex).and_then(|n| n.data().as_vertex()) else {
917 ogeom_bail!(Dangling, "vertex is not in this model");
918 };
919 Ok(vertex.transform(model.datums())?.apply(data.point))
920}
921
922fn edge_samples(
925 model: &Model,
926 edge: &Shape,
927 count: u32,
928 tol: Tolerances,
929) -> OgeomResult<Vec<Point>> {
930 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
931 ogeom_bail!(Dangling, "edge is not in this model");
932 };
933 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
934 ogeom_bail!(Construction, "an edge of the face has no curve in space");
935 };
936 let Some(geometry) = model.geometry().curve(*curve) else {
937 ogeom_bail!(Dangling, "curve is not in this model");
938 };
939 let placement = edge.transform(model.datums())?;
940 (0..=count)
941 .map(|k| {
942 let t = range.0 + (range.1 - range.0) * f64::from(k) / f64::from(count);
943 Ok(placement.apply(geometry.point_at(t, tol)?))
944 })
945 .collect()
946}