Skip to main content

geop_ops_drawing/
view.rs

1//! The directions a part is drawn from, and projecting onto the paper.
2//!
3//! Parts are modelled with `z` up (a sketch on the `xy` plane extrudes
4//! upwards), so the views are the usual ones of a `z`-up modeller: the front
5//! view looks along `+y`, the top view down `-z`, the right view along `-x`,
6//! and the isometric view from the front, right and top at once.
7
8use geop_core_geometry::nurb_curve::{NurbCurve2D, NurbCurve3D};
9use geop_core_math::{
10    geop_error::GeopResult,
11    scalars::Scalar,
12    vector::{Vector2, Vector3},
13};
14use serde::{Deserialize, Serialize};
15
16/// Which way a view looks at the part.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum ViewKind {
20    Front,
21    Top,
22    Right,
23    Left,
24    Bottom,
25    Back,
26    /// Isometric, from the front, right and top.
27    Iso,
28}
29
30impl ViewKind {
31    pub const ALL: [ViewKind; 7] = [
32        ViewKind::Front,
33        ViewKind::Top,
34        ViewKind::Right,
35        ViewKind::Left,
36        ViewKind::Bottom,
37        ViewKind::Back,
38        ViewKind::Iso,
39    ];
40
41    /// Its name, as written in a program and on the command line.
42    pub fn name(self) -> &'static str {
43        match self {
44            ViewKind::Front => "front",
45            ViewKind::Top => "top",
46            ViewKind::Right => "right",
47            ViewKind::Left => "left",
48            ViewKind::Bottom => "bottom",
49            ViewKind::Back => "back",
50            ViewKind::Iso => "iso",
51        }
52    }
53
54    /// The view named `name`, if there is one.
55    pub fn from_name(name: &str) -> Option<ViewKind> {
56        ViewKind::ALL.into_iter().find(|k| k.name() == name)
57    }
58
59    /// The frame it looks along.
60    pub fn frame(self) -> GeopResult<ViewFrame> {
61        let unit = |index: usize, negative: bool| ViewAxis::Unit { index, negative };
62        let (direction, right, up) = match self {
63            ViewKind::Front => (unit(1, false), unit(0, false), unit(2, false)),
64            ViewKind::Back => (unit(1, true), unit(0, true), unit(2, false)),
65            ViewKind::Top => (unit(2, true), unit(0, false), unit(1, false)),
66            ViewKind::Bottom => (unit(2, false), unit(0, false), unit(1, true)),
67            ViewKind::Right => (unit(0, true), unit(1, false), unit(2, false)),
68            ViewKind::Left => (unit(0, false), unit(1, true), unit(2, false)),
69            ViewKind::Iso => {
70                let v = |x: f64, y: f64, z: f64| {
71                    Vector3::from_array([
72                        Scalar::from_f64(x),
73                        Scalar::from_f64(y),
74                        Scalar::from_f64(z),
75                    ])
76                };
77                (
78                    ViewAxis::General(v(-1.0, 1.0, -1.0).normalize()?),
79                    ViewAxis::General(v(1.0, 1.0, 0.0).normalize()?),
80                    ViewAxis::General(v(-1.0, 1.0, 2.0).normalize()?),
81                )
82            }
83        };
84        Ok(ViewFrame {
85            direction,
86            right,
87            up,
88        })
89    }
90}
91
92/// A view of a drawing, as its annotations name it: one looking from a
93/// direction, or the section — `front`, `iso`, `section`.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(into = "String", try_from = "String")]
96pub enum DrawnView {
97    View(ViewKind),
98    Section,
99}
100
101impl DrawnView {
102    pub fn name(self) -> &'static str {
103        match self {
104            DrawnView::View(kind) => kind.name(),
105            DrawnView::Section => "section",
106        }
107    }
108
109    /// Whether it shows lengths as they are: every view but the isometric
110    /// one, which foreshortens them all.
111    pub fn orthographic(self) -> bool {
112        self != DrawnView::View(ViewKind::Iso)
113    }
114}
115
116impl From<DrawnView> for String {
117    fn from(view: DrawnView) -> String {
118        view.name().to_string()
119    }
120}
121
122impl TryFrom<String> for DrawnView {
123    type Error = String;
124
125    fn try_from(name: String) -> Result<Self, String> {
126        match name.as_str() {
127            "section" => Ok(DrawnView::Section),
128            _ => ViewKind::from_name(&name)
129                .map(DrawnView::View)
130                .ok_or_else(|| format!("{name:?} names no view of a drawing")),
131        }
132    }
133}
134
135impl std::fmt::Display for DrawnView {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.write_str(self.name())
138    }
139}
140
141/// A unit vector of a view's frame. A coordinate axis is kept as one, so
142/// projecting along it picks a coordinate rather than multiplying by 1 and
143/// 0, which would widen every interval by rounding (see `AGENTS.md`,
144/// "Construction code is sensitive at the last bit").
145#[derive(Clone, Copy, Debug)]
146pub enum ViewAxis {
147    /// The coordinate axis `index`, negated if `negative`.
148    Unit {
149        index: usize,
150        negative: bool,
151    },
152    General(Vector3),
153}
154
155impl ViewAxis {
156    /// `v`'s component along this axis.
157    pub fn dot(&self, v: &Vector3) -> Scalar {
158        match *self {
159            ViewAxis::Unit { index, negative } => {
160                if negative {
161                    v[index].neg()
162                } else {
163                    v[index]
164                }
165            }
166            ViewAxis::General(axis) => axis.prod_dot(v),
167        }
168    }
169
170    pub fn vector(&self) -> Vector3 {
171        match *self {
172            ViewAxis::Unit { index, negative } => {
173                let mut v = Vector3::zero();
174                v[index] = if negative {
175                    Scalar::ONE.neg()
176                } else {
177                    Scalar::ONE
178                };
179                v
180            }
181            ViewAxis::General(axis) => axis,
182        }
183    }
184}
185
186/// How a view looks: `direction` from the eye into the part, and the
187/// paper's `right` and `up` — right-handed, `right x up = -direction`.
188#[derive(Clone, Copy, Debug)]
189pub struct ViewFrame {
190    pub direction: ViewAxis,
191    pub right: ViewAxis,
192    pub up: ViewAxis,
193}
194
195impl ViewFrame {
196    /// The frame looking along `direction`, with `right` and `up` on the
197    /// paper; `direction` is normalized, and `right` and `up` made
198    /// perpendicular to it.
199    pub fn looking(direction: Vector3, up: Vector3) -> GeopResult<Self> {
200        let d = direction.normalize()?;
201        let right = d.prod_cross(&up).normalize()?;
202        let up = right.prod_cross(&d).normalize()?;
203        Ok(ViewFrame {
204            direction: ViewAxis::General(d),
205            right: ViewAxis::General(right),
206            up: ViewAxis::General(up),
207        })
208    }
209
210    /// Where `p` lands on the paper.
211    pub fn project_point(&self, p: &Vector3) -> Vector2 {
212        Vector2::from_array([self.right.dot(p), self.up.dot(p)])
213    }
214
215    /// `curve` as drawn on the paper: the projection is linear, so it maps
216    /// homogeneous control points to homogeneous control points, keeping
217    /// the knots and weights — the same parametrization, exactly.
218    pub fn project_curve(&self, curve: &NurbCurve3D) -> GeopResult<NurbCurve2D> {
219        let points = curve
220            .control_points
221            .iter()
222            .map(|q| {
223                let h = q.head::<3>();
224                Vector3::from_array([self.right.dot(&h), self.up.dot(&h), q[3]])
225            })
226            .collect();
227        NurbCurve2D::try_new(curve.degree, points, curve.knot_vector.clone())
228    }
229
230    /// The unit vector from the part towards the eye.
231    pub fn toward_eye(&self) -> Vector3 {
232        self.direction.vector().neg()
233    }
234}