Skip to main content

geop_ops_parts/operation/
entity.rs

1//! [`EntityRef`]: how a step refers to geometry it builds on — a vertex,
2//! edge, face or datum of the part by name, or the origin, a world axis or
3//! a base plane — and [`Geometry`], what such an entity is: a point, a
4//! line, a plane, an arc, something round, a curve — or several of these at
5//! once. Which of them an entity is decides what can be built on it.
6
7use geop_core_geometry::{
8    nurb_curve::NurbCurve3D,
9    shape::{Arc, Axis},
10};
11use geop_core_math::{
12    geop_error::{GeopError, GeopResult, WithContext},
13    primitives::CoordinateSystem,
14    scalars::Scalar,
15    vector::Vector3,
16    with_context,
17};
18use geop_core_part::{DatumKind, Part};
19use serde::{Deserialize, Serialize};
20
21/// One of the world's three axes.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23pub enum WorldAxis {
24    X,
25    Y,
26    Z,
27}
28
29impl WorldAxis {
30    fn unit<S: Scalar>(self) -> Vector3<S> {
31        match self {
32            WorldAxis::X => v3(1., 0., 0.),
33            WorldAxis::Y => v3(0., 1., 0.),
34            WorldAxis::Z => v3(0., 0., 1.),
35        }
36    }
37}
38
39fn v3<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
40    Vector3::from_array([x, y, z].map(S::from_f64))
41}
42
43/// Something picked to build on.
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45#[serde(tag = "type")]
46pub enum EntityRef {
47    /// The world origin.
48    Origin,
49    /// A world axis, through the origin.
50    Axis {
51        axis: WorldAxis,
52    },
53    /// A base plane through the origin, named by its normal: the `Z` plane
54    /// is normal to the `z` axis. A sketch's `x`/`y` on it run along world
55    /// `y`/`z` (`X`), `x`/`-z` (`Y`) or `x`/`y` (`Z`).
56    Plane {
57        normal: WorldAxis,
58    },
59    Vertex {
60        name: String,
61    },
62    Edge {
63        name: String,
64    },
65    /// A face: planar, its plane — normal pointing out of its solid, sketch
66    /// origin the world origin's projection onto it and sketch `x` the world
67    /// axis most parallel to it, projected, so sketches on parallel faces
68    /// line up. Round, its axis.
69    Face {
70        name: String,
71    },
72    Datum {
73        name: String,
74    },
75}
76
77impl std::fmt::Display for EntityRef {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            EntityRef::Origin => write!(f, "the origin"),
81            EntityRef::Axis { axis } => write!(f, "the {axis:?} axis"),
82            EntityRef::Plane { normal } => write!(f, "the {normal:?} plane"),
83            EntityRef::Vertex { name } => write!(f, "vertex {name:?}"),
84            EntityRef::Edge { name } => write!(f, "edge {name:?}"),
85            EntityRef::Face { name } => write!(f, "face {name:?}"),
86            EntityRef::Datum { name } => write!(f, "datum {name:?}"),
87        }
88    }
89}
90
91/// Everything an entity can be used as. An entity is usually several at
92/// once: a straight edge is a line and a curve, a circular edge an arc, a
93/// curve and something round, a datum point a point and a frame.
94#[derive(Clone, Debug)]
95pub struct Geometry<S: Scalar> {
96    pub point: Option<Vector3<S>>,
97    /// The line it runs along: a straight edge, an axis.
98    pub line: Option<Axis<S>>,
99    /// The plane it lies in, as a frame with `w` the normal and `u`/`v` a
100    /// sketch's `x`/`y` on it.
101    pub plane: Option<CoordinateSystem<S>>,
102    pub arc: Option<Arc<S>>,
103    /// The axis it turns around: a circular edge, a cylinder, a cone.
104    pub round: Option<Axis<S>>,
105    /// An edge's curve, whatever its shape.
106    pub curve: Option<NurbCurve3D<S>>,
107    /// Its own axes, if it has any: a datum's frame, the world's for the
108    /// origin, axes and base planes.
109    pub frame: Option<CoordinateSystem<S>>,
110}
111
112impl<S: Scalar> Geometry<S> {
113    fn none() -> Self {
114        Self {
115            point: None,
116            line: None,
117            plane: None,
118            arc: None,
119            round: None,
120            curve: None,
121            frame: None,
122        }
123    }
124
125    /// Every role it can fill.
126    pub fn roles(&self) -> Vec<Role> {
127        Role::ALL
128            .into_iter()
129            .filter(|role| role.fits(self))
130            .collect()
131    }
132}
133
134/// What a construction needs an input to be.
135#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
136#[serde(rename_all = "snake_case")]
137pub enum Role {
138    /// A vertex, a datum point, the origin.
139    Point,
140    /// A straight edge, a datum axis, a world axis.
141    Line,
142    /// A planar face, a datum plane, a base plane.
143    Plane,
144    /// Any edge.
145    Edge,
146    /// A circular edge.
147    Circle,
148    /// Something that turns around an axis: a circular edge, a cylindrical,
149    /// conical or spherical face.
150    Round,
151}
152
153impl Role {
154    const ALL: [Role; 6] = [
155        Role::Point,
156        Role::Line,
157        Role::Plane,
158        Role::Edge,
159        Role::Circle,
160        Role::Round,
161    ];
162
163    pub fn fits<S: Scalar>(self, geometry: &Geometry<S>) -> bool {
164        match self {
165            Role::Point => geometry.point.is_some(),
166            Role::Line => geometry.line.is_some(),
167            Role::Plane => geometry.plane.is_some(),
168            Role::Edge => geometry.curve.is_some(),
169            Role::Circle => geometry.arc.is_some(),
170            Role::Round => geometry.round.is_some(),
171        }
172    }
173}
174
175/// A right-handed orthonormal frame at `origin` with `w` along `normal`,
176/// and `u` the world axis most parallel to the plane normal to it,
177/// projected into that plane — so frames with parallel normals line up.
178pub fn frame_along<S: Scalar>(
179    origin: Vector3<S>,
180    normal: &Vector3<S>,
181) -> GeopResult<CoordinateSystem<S>> {
182    let n = normal.normalize()?;
183    let axis = (0..3)
184        .min_by(|&a, &b| n[a].to_f64().abs().total_cmp(&n[b].to_f64().abs()))
185        .expect("three axes");
186    let mut a = Vector3::zero();
187    a[axis] = S::ONE;
188    let u = a.sub(&n.prod_scalar(n.prod_dot(&a))).normalize()?;
189    let v = n.prod_cross(&u);
190    CoordinateSystem::try_new(origin, u, v, n)
191}
192
193/// The world's own axes, moved to `origin`.
194pub fn world_frame<S: Scalar>(origin: Vector3<S>) -> GeopResult<CoordinateSystem<S>> {
195    CoordinateSystem::try_new(origin, v3(1., 0., 0.), v3(0., 1., 0.), v3(0., 0., 1.))
196}
197
198/// The frame of the base plane normal to `normal`.
199fn base_plane<S: Scalar>(normal: WorldAxis) -> GeopResult<CoordinateSystem<S>> {
200    let origin = Vector3::zero();
201    match normal {
202        WorldAxis::X => {
203            CoordinateSystem::try_new(origin, v3(0., 1., 0.), v3(0., 0., 1.), v3(1., 0., 0.))
204        }
205        WorldAxis::Y => {
206            CoordinateSystem::try_new(origin, v3(1., 0., 0.), v3(0., 0., -1.), v3(0., 1., 0.))
207        }
208        WorldAxis::Z => world_frame(origin),
209    }
210}
211
212impl EntityRef {
213    /// What the entity is in `part`. Fails if the part has no such entity.
214    pub fn resolve<S: Scalar>(&self, part: &Part<S>) -> GeopResult<Geometry<S>> {
215        let ctx = with_context!("resolving {self}");
216        let mut g = Geometry::none();
217        match self {
218            EntityRef::Origin => {
219                g.point = Some(Vector3::zero());
220                g.frame = Some(world_frame(Vector3::zero())?);
221            }
222            EntityRef::Axis { axis } => {
223                let direction = axis.unit();
224                g.line = Some(Axis::try_new(Vector3::zero(), direction)?);
225                g.frame = Some(frame_along(Vector3::zero(), &direction)?);
226            }
227            EntityRef::Plane { normal } => {
228                let frame = base_plane(*normal)?;
229                g.plane = Some(frame.clone());
230                g.frame = Some(frame);
231            }
232            EntityRef::Vertex { name } => {
233                let id = part.vertex_id(name).with_context(ctx)?;
234                g.point = Some(part.topology().get_vertex(id).with_context(ctx)?.point);
235            }
236            EntityRef::Edge { name } => {
237                let id = part.edge_id(name).with_context(ctx)?;
238                let curve = part
239                    .topology()
240                    .get_edge(id)
241                    .with_context(ctx)?
242                    .curve
243                    .clone();
244                g.line = curve.as_line().with_context(ctx)?;
245                g.arc = curve.as_arc().with_context(ctx)?;
246                g.round = g.arc.as_ref().map(|arc| arc.circle.axis());
247                g.curve = Some(curve);
248            }
249            EntityRef::Face { name } => {
250                let id = part.face_id(name).with_context(ctx)?;
251                let surface = &part.topology().get_face(id).with_context(ctx)?.surface;
252                if let Some(plane) = surface.as_plane().with_context(ctx)? {
253                    let origin = plane.project(&Vector3::zero());
254                    g.plane = Some(frame_along(origin, &plane.normal)?);
255                }
256                g.round = surface.axis_of_revolution().with_context(ctx)?;
257            }
258            EntityRef::Datum { name } => {
259                let id = part.datum_id(name).with_context(ctx)?;
260                let datum = part.datum(id).with_context(ctx)?;
261                let frame = datum.frame.clone();
262                match datum.kind {
263                    DatumKind::Point => g.point = Some(*frame.origin()),
264                    DatumKind::Axis => g.line = Some(Axis::try_new(*frame.origin(), *frame.w())?),
265                    DatumKind::Plane => g.plane = Some(frame.clone()),
266                }
267                g.frame = Some(frame);
268            }
269        }
270        Ok(g)
271    }
272}
273
274/// The frame of the plane `plane` refers to in `part`: `u`/`v` a sketch's
275/// `x`/`y` on it, `w = u x v` its normal. Fails if it is not a plane.
276pub fn resolve_plane<S: Scalar>(
277    part: &Part<S>,
278    plane: &EntityRef,
279) -> GeopResult<CoordinateSystem<S>> {
280    plane
281        .resolve(part)?
282        .plane
283        .ok_or_else(|| GeopError::new(format!("{plane} is not planar")))
284}