Skip to main content

geop_core_sketch/
dimension.rs

1//! How a dimension is drawn in a plane, in plain numbers ([`Measure`]): the
2//! lines that show what it measures with its value at a label point, and
3//! where their arrowheads go. A sketch's dimensions and a drawing's are
4//! both drawn by it, so the two look alike and are placed alike: a value
5//! dragged anywhere, its lines following.
6//!
7//! Like [`crate::plain`], this is only ever what is shown — where a label
8//! goes is the designer's free choice — never a result the kernel reasons
9//! about.
10
11use crate::plain::{P2, add, distance, dot, perp, rotate, scale, sub, unit};
12
13/// What a dimension measures, where it is in the plane.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum Measure {
16    /// From `a` to `b`, along the direction `along`: an aligned distance
17    /// along `b - a`, a horizontal or a vertical one along an axis.
18    Linear { a: P2, b: P2, along: P2 },
19    /// The radius of a circle — or, `diameter`, its diameter.
20    Radial {
21        center: P2,
22        radius: f64,
23        diameter: bool,
24    },
25    /// The angle at `vertex` between its sides along the directions `from`
26    /// and `to`, measured turning `sweep` radians counter-clockwise from
27    /// `from`.
28    Angular {
29        vertex: P2,
30        from: P2,
31        to: P2,
32        sweep: f64,
33    },
34}
35
36/// How many pieces an angle's arc is drawn in.
37const ARC_PIECES: usize = 32;
38
39impl Measure {
40    /// The lines it is drawn with when its value is shown at `label`: for a
41    /// linear one, extension lines from what it measures out to a
42    /// dimension line through the label, along what it measures — and on
43    /// to the label if it is beside them; for a radial one, a leader from
44    /// the centre — a diameter's from the far rim — across the circle, out
45    /// to the label if it is beyond the rim; for an angle, an arc about its
46    /// vertex through the label, and its sides out to the arc. None for
47    /// what has no direction to be drawn along.
48    pub fn lines(&self, label: P2) -> Vec<Vec<P2>> {
49        match *self {
50            Measure::Linear { a, b, along } => {
51                let Some((a2, b2, u)) = Self::feet(a, b, along, label) else {
52                    return Vec::new();
53                };
54                // The dimension line runs between the extension lines, and
55                // on to the label if it is beside them.
56                let t = |q: P2| dot(sub(q, a2), u);
57                let (lo, hi) = (t(b2).min(0.0).min(t(label)), t(b2).max(0.0).max(t(label)));
58                vec![
59                    vec![a, a2],
60                    vec![b, b2],
61                    vec![add(a2, scale(u, lo)), add(a2, scale(u, hi))],
62                ]
63            }
64            Measure::Radial {
65                center,
66                radius,
67                diameter,
68            } => {
69                let Some(d) = unit(sub(label, center)) else {
70                    return Vec::new();
71                };
72                let rim = add(center, scale(d, radius));
73                let from = if diameter {
74                    add(center, scale(d, -radius))
75                } else {
76                    center
77                };
78                // Out to the label, if it is beyond the rim.
79                let to = if dot(sub(label, center), d) > radius {
80                    label
81                } else {
82                    rim
83                };
84                vec![vec![from, to]]
85            }
86            Measure::Angular {
87                vertex, from, to, ..
88            } => {
89                let r = distance(label, vertex);
90                let side = |d: P2| vec![vertex, add(vertex, scale(unit(d).unwrap_or(d), r))];
91                vec![self.arc(r), side(from), side(to)]
92            }
93        }
94    }
95
96    /// Its arrowheads, as drafting draws them, with its value at `label`:
97    /// each tip, and the unit direction it points in. A linear dimension's
98    /// point out from between its extension lines, a radius' out at the
99    /// rim — a diameter's at both rims — and an angle's along its arc, out
100    /// at either end.
101    pub fn arrows(&self, label: P2) -> Vec<(P2, P2)> {
102        match *self {
103            Measure::Linear { a, b, along, .. } => {
104                let Some((a2, b2, _)) = Self::feet(a, b, along, label) else {
105                    return Vec::new();
106                };
107                match unit(sub(b2, a2)) {
108                    Some(d) => vec![(a2, scale(d, -1.0)), (b2, d)],
109                    None => Vec::new(),
110                }
111            }
112            Measure::Radial {
113                center,
114                radius,
115                diameter,
116            } => {
117                let Some(d) = unit(sub(label, center)) else {
118                    return Vec::new();
119                };
120                let mut arrows = vec![(add(center, scale(d, radius)), d)];
121                if diameter {
122                    arrows.push((add(center, scale(d, -radius)), scale(d, -1.0)));
123                }
124                arrows
125            }
126            Measure::Angular {
127                vertex,
128                from,
129                sweep,
130                ..
131            } => {
132                let r = distance(label, vertex);
133                let (Some(from), true) = (unit(from), r > 0.0 && sweep != 0.0) else {
134                    return Vec::new();
135                };
136                let arc = self.arc(r);
137                let turn = sweep.signum();
138                let end = rotate(from, sweep);
139                vec![
140                    (arc[0], scale(perp(from), -turn)),
141                    (arc[arc.len() - 1], scale(perp(end), turn)),
142                ]
143            }
144        }
145    }
146
147    /// Where a linear dimension's extension lines meet its dimension line
148    /// through `label`, and the unit direction it runs in.
149    fn feet(a: P2, b: P2, along: P2, label: P2) -> Option<(P2, P2, P2)> {
150        let u = unit(along)?;
151        let n = perp(u);
152        let lift = |q: P2| add(q, scale(n, dot(sub(label, q), n)));
153        Some((lift(a), lift(b), u))
154    }
155
156    /// An angle's arc, `r` from its vertex.
157    fn arc(&self, r: f64) -> Vec<P2> {
158        let Measure::Angular {
159            vertex,
160            from,
161            sweep,
162            ..
163        } = *self
164        else {
165            return Vec::new();
166        };
167        let start = from[1].atan2(from[0]);
168        (0..=ARC_PIECES)
169            .map(|i| {
170                let t = start + sweep * i as f64 / ARC_PIECES as f64;
171                add(vertex, [r * t.cos(), r * t.sin()])
172            })
173            .collect()
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn near(a: P2, b: P2) -> bool {
182        distance(a, b) < 1e-12
183    }
184
185    /// A horizontal distance drawn above what it measures: extension lines
186    /// up to the label's height, the dimension line between them and on to
187    /// a label beside them, arrows pointing out at its ends.
188    #[test]
189    fn a_linear_dimension_reaches_its_label() {
190        let m = Measure::Linear {
191            a: [0.0, 0.0],
192            b: [4.0, 1.0],
193            along: [1.0, 0.0],
194        };
195        let lines = m.lines([6.0, 3.0]);
196        assert_eq!(lines[0], vec![[0.0, 0.0], [0.0, 3.0]]);
197        assert_eq!(lines[1], vec![[4.0, 1.0], [4.0, 3.0]]);
198        assert_eq!(lines[2], vec![[0.0, 3.0], [6.0, 3.0]]);
199        let arrows = m.arrows([6.0, 3.0]);
200        assert_eq!(
201            arrows,
202            vec![([0.0, 3.0], [-1.0, 0.0]), ([4.0, 3.0], [1.0, 0.0])]
203        );
204    }
205
206    /// A diameter's leader crosses the circle from rim to rim, an arrow at
207    /// each; a radius' runs from the centre, one arrow at the rim.
208    #[test]
209    fn radial_dimensions_cross_their_circle() {
210        let diameter = Measure::Radial {
211            center: [1.0, 1.0],
212            radius: 2.0,
213            diameter: true,
214        };
215        assert_eq!(
216            diameter.lines([1.0, 5.0]),
217            vec![vec![[1.0, -1.0], [1.0, 5.0]]]
218        );
219        assert_eq!(diameter.arrows([1.0, 5.0]).len(), 2);
220        let radius = Measure::Radial {
221            center: [1.0, 1.0],
222            radius: 2.0,
223            diameter: false,
224        };
225        assert_eq!(radius.lines([1.0, 2.0]), vec![vec![[1.0, 1.0], [1.0, 3.0]]]);
226        assert_eq!(radius.arrows([1.0, 2.0]), vec![([1.0, 3.0], [0.0, 1.0])]);
227    }
228
229    /// A right angle's arc runs through its label, from one side to the
230    /// other, its arrows pointing out along it.
231    #[test]
232    fn an_angle_arcs_through_its_label() {
233        let m = Measure::Angular {
234            vertex: [0.0, 0.0],
235            from: [1.0, 0.0],
236            to: [0.0, 1.0],
237            sweep: std::f64::consts::FRAC_PI_2,
238        };
239        let label = [2.0_f64.sqrt(), 2.0_f64.sqrt()];
240        let lines = m.lines(label);
241        assert!(near(lines[0][0], [2.0, 0.0]) && near(*lines[0].last().unwrap(), [0.0, 2.0]));
242        assert!(near(lines[2][1], [0.0, 2.0]));
243        let arrows = m.arrows(label);
244        assert!(near(arrows[0].1, [0.0, -1.0]) && near(arrows[1].1, [-1.0, 0.0]));
245    }
246}