Skip to main content

ogeom_offset/sweep/
placement.rs

1//! Bringing a profile onto a spine before it is swept.
2//!
3//! A pipe shell sweeps a profile standing square to the spine at its
4//! start, through the start point. A profile drawn elsewhere, or leaning,
5//! is brought there by a rigid motion about its centroid (the centre of
6//! the area it bounds): turned by the least rotation that stands its plane
7//! square to the spine's start tangent, and moved so its centroid lands on
8//! the spine's start point.
9//!
10//! A pipe shell may instead bring its profile into contact itself, as
11//! [`PipeContact`] asks: moved along its plane's normal onto the spine's
12//! start, and turned about that point square to the start tangent. That
13//! needs only the profile's plane, so it serves open wires as well as
14//! faces and closed wires, and it keeps the profile's offset from the
15//! spine within its plane.
16
17use ogeom_algo::{Built, find_plane, surface_properties};
18use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
19use ogeom_geom::{Curve3d as _, PlaneSurface};
20use ogeom_math::{Axis, Direction, Point, Transform, Vector};
21use ogeom_mesh::Deflection;
22use ogeom_topo::{Model, Shape, ShapeType};
23
24use super::{moved_ring, realized_profile};
25
26/// How a profile is brought onto a spine by [`place_on_spine`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct PipePlacement {
29    /// Move the profile, unturned, so its centroid stands on the spine's
30    /// start point.
31    pub contact: bool,
32    /// Turn the profile about its centroid, by the least rotation, until
33    /// its plane stands square to the spine's tangent at its start.
34    pub correction: bool,
35}
36
37/// How a pipe shell brings its profile onto the spine before sweeping it,
38/// a rigid motion of the profile defined by the profile's plane and the
39/// spine's start alone.
40///
41/// The contact point is the foot of the spine's start point on the
42/// profile's plane. It is one point for any planar profile, an open wire
43/// as much as a face, and a face and its outer wire share it; the
44/// profile's own point nearest the start would not be (every point of a
45/// ring about the start is nearest at once, and a face's nearest point
46/// can lie inside it where its boundary's cannot).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum PipeContact {
49    /// The profile is swept where it stands: square to the spine at its
50    /// start and through the start point, or refused.
51    #[default]
52    AsPlaced,
53    /// The profile is moved, unturned, along its plane's normal by the
54    /// distance from its plane to the spine's start point, so that the
55    /// contact point lands on the start point. A profile already within
56    /// the confusion distance of the start is swept as it stands.
57    Contact,
58    /// As `Contact`, then the profile is turned about the spine's start
59    /// point by the least rotation standing its plane square to the
60    /// spine's start tangent: about the axis square to both, through the
61    /// angle between the tangent and the plane's normal taken whichever
62    /// way round the normal is nearer the tangent. A plane has no side, so
63    /// a normal pointing back along the tangent is already square and the
64    /// profile is not turned. A plane holding the tangent (to an angle's
65    /// tolerance) is a quarter turn from square either way round, and is
66    /// refused.
67    Correction,
68}
69
70/// A planar profile (a face, or a closed wire) moved rigidly onto the start
71/// of `spine` as `placement` asks: turned about its centroid until it
72/// stands square to the spine's start tangent where `correction`, and
73/// moved so its centroid is the spine's start point where `contact`. The
74/// result is a fresh profile, ready for [`make_pipe_shell`](super::make_pipe_shell)
75/// and the sweeps like it; the history records it as the profile
76/// modified.
77///
78/// # Errors
79///
80/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
81/// the profile is not a planar face or closed wire, or the spine is not an
82/// edge or a wire with a tangent at its start.
83pub fn place_on_spine(
84    model: &mut Model,
85    profile: &Shape,
86    spine: &Shape,
87    placement: PipePlacement,
88    tol: Tolerances,
89) -> OgeomResult<Built> {
90    let kind = model.kind_of(profile)?;
91    if !matches!(kind, ShapeType::Face | ShapeType::Wire) {
92        ogeom_bail!(
93            Construction,
94            "a profile is a planar face or closed wire, not a {kind:?}"
95        );
96    }
97    let Some(plane) = find_plane(model, profile, tol)? else {
98        ogeom_bail!(Construction, "a pipe shell sweeps a planar profile");
99    };
100    let (start, tangent) = spine_start(model, spine, tol)?;
101    let centre = centroid(model, profile, kind, &plane, tol)?;
102    let mut motion = Transform::IDENTITY;
103    if placement.correction
104        && let Some(turn) = least_turn(plane.normal().vector(), tangent, centre, tol)?
105    {
106        motion = turn;
107    }
108    if placement.contact {
109        motion = Transform::translation(start - centre) * motion;
110    }
111    let placed = match kind {
112        ShapeType::Face => realized_profile(model, profile, &motion, tol)?,
113        _ => moved_ring(model, profile, &motion, tol)?,
114    };
115    let mut built = Built::new(placed.clone(), ogeom_algo::History::new());
116    built.history.modify(profile, placed);
117    Ok(built)
118}
119
120/// The least rotation about `about` standing a plane of normal `normal`
121/// square to the unit `tangent`, or `None` where it stands square already
122/// to an angle's tolerance. The plane faces whichever way round is nearer
123/// the tangent.
124fn least_turn(
125    normal: Vector,
126    tangent: Vector,
127    about: Point,
128    tol: Tolerances,
129) -> OgeomResult<Option<Transform>> {
130    let n = if normal.dot(tangent) < 0.0 {
131        -normal
132    } else {
133        normal
134    };
135    let axis = n.cross(tangent);
136    if axis.magnitude() <= tol.angular() {
137        return Ok(None);
138    }
139    let about = Axis {
140        location: about,
141        direction: Direction::new(axis, tol)?,
142    };
143    Ok(Some(Transform::rotation(
144        about,
145        axis.magnitude().atan2(n.dot(tangent)),
146    )))
147}
148
149/// The profile a pipe shell sweeps under `contact`: `profile` itself where
150/// it is swept as placed or the motion moves nothing, else a fresh profile
151/// moved rigidly as [`PipeContact`] defines.
152///
153/// # Errors
154///
155/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
156/// the profile is not a planar wire or face, if the spine has no tangent
157/// at its start, or if a correction is asked of a profile whose plane
158/// holds the start tangent.
159pub(crate) fn profile_in_contact(
160    model: &mut Model,
161    profile: &Shape,
162    spine: &Shape,
163    contact: PipeContact,
164    tol: Tolerances,
165) -> OgeomResult<Shape> {
166    if contact == PipeContact::AsPlaced {
167        return Ok(profile.clone());
168    }
169    let kind = model.kind_of(profile)?;
170    if !matches!(kind, ShapeType::Face | ShapeType::Wire) {
171        ogeom_bail!(
172            Construction,
173            "a pipe shell sweeps a planar wire or face, not a {kind:?}"
174        );
175    }
176    let Some(plane) = find_plane(model, profile, tol)? else {
177        ogeom_bail!(
178            Construction,
179            "a profile is brought into contact along its plane's normal; this \
180             one is not planar"
181        );
182    };
183    let (start, tangent) = spine_start(model, spine, tol)?;
184    let normal = plane.normal().vector();
185    let shift = normal * (start - plane.origin()).dot(normal);
186    let mut motion = Transform::translation(shift);
187    let mut moves = shift.magnitude() > tol.confusion();
188    if contact == PipeContact::Correction {
189        if normal.dot(tangent).abs() <= tol.angular() {
190            ogeom_bail!(
191                Construction,
192                "the profile's plane holds the spine's start tangent: a quarter \
193                 turn either way round stands it square, so its correction is \
194                 not one turn"
195            );
196        }
197        if let Some(turn) = least_turn(normal, tangent, start, tol)? {
198            motion = turn * motion;
199            moves = true;
200        }
201    }
202    if !moves {
203        return Ok(profile.clone());
204    }
205    match kind {
206        ShapeType::Face => realized_profile(model, profile, &motion, tol),
207        _ => moved_ring(model, profile, &motion, tol),
208    }
209}
210
211/// The spine's start point and its unit tangent there, in the direction
212/// of travel, read exactly off its first edge.
213fn spine_start(model: &Model, spine: &Shape, tol: Tolerances) -> OgeomResult<(Point, Vector)> {
214    let first = match model.kind_of(spine)? {
215        ShapeType::Edge => spine.clone(),
216        ShapeType::Wire => match model.ordered_children_of(spine)?.first() {
217            Some(edge) => edge.clone(),
218            None => ogeom_bail!(Construction, "the spine has no edge"),
219        },
220        other => ogeom_bail!(
221            Construction,
222            "a spine is an edge or a wire, not a {other:?}"
223        ),
224    };
225    let Some((curve, range)) = ogeom_algo::placed_edge_curve(model, &first, tol)? else {
226        ogeom_bail!(Construction, "an edge has no curve");
227    };
228    let reversed = first.orientation() == ogeom_topo::Orientation::Reversed;
229    let t = if reversed { range.1 } else { range.0 };
230    let d = curve.d1_at(t, tol)?;
231    let d = if reversed { -d } else { d };
232    if d.magnitude() <= tol.confusion() {
233        ogeom_bail!(Construction, "the spine has no tangent at its start");
234    }
235    Ok((curve.point_at(t, tol)?, d / d.magnitude()))
236}
237
238/// The centroid of the area a planar profile bounds: a face's own, or a
239/// closed wire's as the face it bounds on `plane`.
240fn centroid(
241    model: &mut Model,
242    profile: &Shape,
243    kind: ShapeType,
244    plane: &ogeom_math::Plane,
245    tol: Tolerances,
246) -> OgeomResult<Point> {
247    let face = if kind == ShapeType::Face {
248        profile.clone()
249    } else {
250        ogeom_algo::make_face(
251            model,
252            PlaneSurface::new(*plane).into(),
253            std::slice::from_ref(profile),
254            tol,
255        )?
256        .shape
257    };
258    // Rebuilt with its pcurves on the plane, the face is integrated exactly
259    // where its edges allow; else over a mesh as fine as the model's
260    // confusion.
261    let charted = realized_profile(model, &face, &Transform::IDENTITY, tol)?;
262    let chord = Deflection::with_chord(tol.confusion())?;
263    Ok(surface_properties(model, &charted, chord, tol)?.centre)
264}