1use core::f64::consts::{PI, TAU};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{Arc, GeoResult, GeometryError, Point, Segment, Vector, normalize_angle};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum LinearKind {
16 #[default]
18 Aligned,
19 Horizontal,
21 Vertical,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
27pub struct DimensionStyle {
28 pub extension_gap: f64,
30 pub extension_overshoot: f64,
32 pub arrow_size: f64,
34 pub text_height: f64,
36}
37
38impl Default for DimensionStyle {
39 fn default() -> Self {
40 Self { extension_gap: 1.0, extension_overshoot: 2.0, arrow_size: 3.0, text_height: 3.5 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
46pub struct Arrow {
47 pub tip: Point,
49 pub direction: Vector,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct DimensionGeometry {
56 pub lines: Vec<Segment>,
58 pub arc: Option<Arc>,
60 pub arrows: Vec<Arrow>,
62 pub text_position: Point,
64 pub text_rotation: f64,
66 pub value: f64,
68}
69
70fn readable(angle: f64) -> f64 {
71 let a = crate::normalize_angle_signed(angle);
72 if a > PI / 2.0 + 1e-12 {
73 a - PI
74 } else if a <= -PI / 2.0 + 1e-12 {
75 a + PI
76 } else {
77 a
78 }
79}
80
81pub fn linear(
83 p1: Point,
84 p2: Point,
85 through: Point,
86 kind: LinearKind,
87 style: DimensionStyle,
88) -> GeoResult<DimensionGeometry> {
89 if !(p1.is_finite() && p2.is_finite() && through.is_finite()) {
90 return Err(GeometryError::NonFinite("dimension points"));
91 }
92 let u = match kind {
93 LinearKind::Aligned => (p2 - p1).normalize().ok_or(GeometryError::Degenerate("dimension points coincide"))?,
94 LinearKind::Horizontal => Vector::new(1.0, 0.0),
95 LinearKind::Vertical => Vector::new(0.0, 1.0),
96 };
97 let n = u.perp();
98 let d1 = (through - p1).dot(n);
99 let d2 = (through - p2).dot(n);
100 let q1 = p1 + n * d1;
101 let q2 = p2 + n * d2;
102 let value = (p2 - p1).dot(u).abs();
103 if value == 0.0 {
104 return Err(GeometryError::Degenerate("projected dimension length is zero"));
105 }
106 let ext = |p: Point, q: Point| -> Option<Segment> {
107 let v = q - p;
108 let len = v.length();
109 let dir = v.normalize()?;
110 (len > style.extension_gap)
111 .then(|| Segment::new(p + dir * style.extension_gap, q + dir * style.extension_overshoot))
112 };
113 let mut lines: Vec<Segment> = [ext(p1, q1), ext(p2, q2)].into_iter().flatten().collect();
114 lines.push(Segment::new(q1, q2));
115 let along = (q2 - q1).normalize().unwrap_or(u);
116 Ok(DimensionGeometry {
117 lines,
118 arc: None,
119 arrows: vec![Arrow { tip: q1, direction: -along }, Arrow { tip: q2, direction: along }],
120 text_position: q1.midpoint(q2) + n * (style.text_height * 0.75 * if d1 < 0.0 { -1.0 } else { 1.0 }),
122 text_rotation: readable(u.angle()),
123 value,
124 })
125}
126
127pub fn angular(vertex: Point, a: Point, b: Point, radius: f64, style: DimensionStyle) -> GeoResult<DimensionGeometry> {
130 let va = (a - vertex).normalize().ok_or(GeometryError::Degenerate("angular ray a"))?;
131 let vb = (b - vertex).normalize().ok_or(GeometryError::Degenerate("angular ray b"))?;
132 if !(radius.is_finite() && radius > 0.0) {
133 return Err(GeometryError::InvalidArgument("angular dimension radius must be > 0"));
134 }
135 let start = va.angle();
136 let sweep = normalize_angle(vb.angle() - start);
137 if sweep == 0.0 {
138 return Err(GeometryError::Degenerate("angular rays coincide"));
139 }
140 let arc = Arc::new(vertex, radius, start, sweep)?;
141 let mid_dir = Vector::from_angle(start + sweep * 0.5);
142 Ok(DimensionGeometry {
143 lines: vec![
144 Segment::new(
145 vertex + va * style.extension_gap.min(radius * 0.5),
146 vertex + va * (radius + style.extension_overshoot),
147 ),
148 Segment::new(
149 vertex + vb * style.extension_gap.min(radius * 0.5),
150 vertex + vb * (radius + style.extension_overshoot),
151 ),
152 ],
153 arc: Some(arc),
154 arrows: vec![
155 Arrow { tip: arc.start_point(), direction: -arc.tangent_at(0.0) },
156 Arrow { tip: arc.end_point(), direction: arc.tangent_at(1.0) },
157 ],
158 text_position: vertex + mid_dir * (radius + style.text_height),
159 text_rotation: readable(mid_dir.angle() - PI / 2.0),
160 value: sweep.min(TAU),
161 })
162}
163
164pub fn radial(center: Point, radius: f64, angle: f64, style: DimensionStyle) -> GeoResult<DimensionGeometry> {
166 if !(radius.is_finite() && radius > 0.0 && center.is_finite() && angle.is_finite()) {
167 return Err(GeometryError::InvalidArgument("radial dimension input"));
168 }
169 let dir = Vector::from_angle(angle);
170 let tip = center + dir * radius;
171 Ok(DimensionGeometry {
172 lines: vec![Segment::new(center, tip)],
173 arc: None,
174 arrows: vec![Arrow { tip, direction: dir }],
175 text_position: center + dir * (radius * 0.5) + dir.perp() * (style.text_height * 0.75),
176 text_rotation: readable(angle),
177 value: radius,
178 })
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn aligned_dimension_value_and_offset() {
187 let d = linear(
188 Point::new(0.0, 0.0),
189 Point::new(30.0, 40.0),
190 Point::new(-8.0, 6.0),
191 LinearKind::Aligned,
192 DimensionStyle::default(),
193 )
194 .unwrap();
195 assert!((d.value - 50.0).abs() < 1e-12);
196 let dim_line = d.lines.last().unwrap();
197 assert!((dim_line.length() - 50.0).abs() < 1e-12);
199 assert!((dim_line.a.distance(Point::ORIGIN) - 10.0).abs() < 1e-12);
200 }
201
202 #[test]
203 fn horizontal_projection() {
204 let d = linear(
205 Point::new(0.0, 0.0),
206 Point::new(30.0, 40.0),
207 Point::new(0.0, 60.0),
208 LinearKind::Horizontal,
209 DimensionStyle::default(),
210 )
211 .unwrap();
212 assert!((d.value - 30.0).abs() < 1e-12);
213 assert!((d.text_rotation).abs() < 1e-12);
214 }
215
216 #[test]
217 fn text_stays_readable() {
218 let d = linear(
219 Point::new(10.0, 0.0),
220 Point::new(0.0, 0.0),
221 Point::new(5.0, 5.0),
222 LinearKind::Aligned,
223 DimensionStyle::default(),
224 )
225 .unwrap();
226 assert!(d.text_rotation.abs() < 1e-12, "{}", d.text_rotation);
227 }
228
229 #[test]
230 fn angular_value() {
231 let d =
232 angular(Point::ORIGIN, Point::new(1.0, 0.0), Point::new(0.0, 2.0), 5.0, DimensionStyle::default()).unwrap();
233 assert!((d.value - PI / 2.0).abs() < 1e-12);
234 assert!(d.arc.is_some());
235 }
236}