1pub(crate) const ARROWHEAD_LENGTH: f64 = 14.0;
5pub(crate) const ARROWHEAD_ANGLE: f64 = 0.45;
6
7pub(crate) const HACHURE_LINE_WIDTH: f64 = 1.5;
9
10pub(crate) fn normalize_bounds(x: f64, y: f64, w: f64, h: f64) -> (f64, f64, f64, f64) {
13 let nx = if w < 0.0 { x + w } else { x };
14 let ny = if h < 0.0 { y + h } else { y };
15 (nx, ny, w.abs(), h.abs())
16}
17
18pub(crate) struct ArrowheadPoints {
20 pub(crate) tip_x: f64,
21 pub(crate) tip_y: f64,
22 pub(crate) left_x: f64,
23 pub(crate) left_y: f64,
24 pub(crate) right_x: f64,
25 pub(crate) right_y: f64,
26}
27
28pub(crate) fn compute_arrowhead(
33 tip_x: f64,
34 tip_y: f64,
35 from_x: f64,
36 from_y: f64,
37 length: f64,
38 spread: f64,
39) -> ArrowheadPoints {
40 let angle = (tip_y - from_y).atan2(tip_x - from_x);
41 ArrowheadPoints {
42 tip_x,
43 tip_y,
44 left_x: tip_x - length * (angle - spread).cos(),
45 left_y: tip_y - length * (angle - spread).sin(),
46 right_x: tip_x - length * (angle + spread).cos(),
47 right_y: tip_y - length * (angle + spread).sin(),
48 }
49}
50
51pub(crate) struct HachureLine {
53 pub(crate) x1: f64,
54 pub(crate) y1: f64,
55 pub(crate) x2: f64,
56 pub(crate) y2: f64,
57}
58
59pub(crate) fn generate_hachure_lines(
65 cx: f64,
66 cy: f64,
67 width: f64,
68 height: f64,
69 gap: f64,
70 angle: f64,
71) -> Vec<HachureLine> {
72 let diag = (width * width + height * height).sqrt();
73 let cos_a = angle.cos();
74 let sin_a = angle.sin();
75
76 let mut lines = Vec::new();
77 let mut d = -diag;
78 while d < diag {
79 lines.push(HachureLine {
80 x1: cx + d * cos_a - (-diag) * sin_a,
81 y1: cy + d * sin_a + (-diag) * cos_a,
82 x2: cx + d * cos_a - diag * sin_a,
83 y2: cy + d * sin_a + diag * cos_a,
84 });
85 d += gap;
86 }
87 lines
88}
89
90use crate::element::Element;
91
92#[derive(Debug, Clone)]
94pub struct ConnectionPoint {
95 pub x: f64,
96 pub y: f64,
97}
98
99pub fn connection_points(el: &Element) -> Vec<ConnectionPoint> {
103 match el {
104 Element::Rectangle(e) => {
105 let (x, y, w, h) = normalize_bounds(e.x, e.y, e.width, e.height);
106 rectangle_connection_points(x, y, w, h)
107 }
108 Element::Ellipse(e) => {
109 let (x, y, w, h) = normalize_bounds(e.x, e.y, e.width, e.height);
110 ellipse_connection_points(x, y, w, h)
111 }
112 Element::Diamond(e) => {
113 let (x, y, w, h) = normalize_bounds(e.x, e.y, e.width, e.height);
114 diamond_connection_points(x, y, w, h)
115 }
116 _ => Vec::new(),
117 }
118}
119
120fn rectangle_connection_points(x: f64, y: f64, w: f64, h: f64) -> Vec<ConnectionPoint> {
121 vec![
122 ConnectionPoint { x: x + w / 2.0, y }, ConnectionPoint {
125 x: x + w,
126 y: y + h / 2.0,
127 }, ConnectionPoint {
129 x: x + w / 2.0,
130 y: y + h,
131 }, ConnectionPoint { x, y: y + h / 2.0 }, ConnectionPoint { x, y }, ConnectionPoint { x: x + w, y }, ConnectionPoint { x: x + w, y: y + h }, ConnectionPoint { x, y: y + h }, ]
139}
140
141fn ellipse_connection_points(x: f64, y: f64, w: f64, h: f64) -> Vec<ConnectionPoint> {
142 let cx = x + w / 2.0;
143 let cy = y + h / 2.0;
144 let rx = w / 2.0;
145 let ry = h / 2.0;
146 let cos45 = std::f64::consts::FRAC_PI_4.cos();
147 let sin45 = std::f64::consts::FRAC_PI_4.sin();
148 vec![
149 ConnectionPoint { x: cx, y: cy - ry }, ConnectionPoint { x: cx + rx, y: cy }, ConnectionPoint { x: cx, y: cy + ry }, ConnectionPoint { x: cx - rx, y: cy }, ConnectionPoint {
156 x: cx + rx * cos45,
157 y: cy - ry * sin45,
158 }, ConnectionPoint {
160 x: cx + rx * cos45,
161 y: cy + ry * sin45,
162 }, ConnectionPoint {
164 x: cx - rx * cos45,
165 y: cy + ry * sin45,
166 }, ConnectionPoint {
168 x: cx - rx * cos45,
169 y: cy - ry * sin45,
170 }, ]
172}
173
174fn diamond_connection_points(x: f64, y: f64, w: f64, h: f64) -> Vec<ConnectionPoint> {
175 let cx = x + w / 2.0;
176 let cy = y + h / 2.0;
177 vec![
178 ConnectionPoint { x: cx, y }, ConnectionPoint { x: x + w, y: cy }, ConnectionPoint { x: cx, y: y + h }, ConnectionPoint { x, y: cy }, ConnectionPoint {
185 x: cx + w / 4.0,
186 y: y + h / 4.0,
187 }, ConnectionPoint {
189 x: cx + w / 4.0,
190 y: cy + h / 4.0,
191 }, ConnectionPoint {
193 x: cx - w / 4.0,
194 y: cy + h / 4.0,
195 }, ConnectionPoint {
197 x: cx - w / 4.0,
198 y: y + h / 4.0,
199 }, ]
201}
202
203pub fn find_nearest_snap_point(
208 elements: &[Element],
209 wx: f64,
210 wy: f64,
211 threshold: f64,
212 exclude_id: &str,
213) -> Option<(String, f64, f64)> {
214 let mut best: Option<(String, f64, f64, f64)> = None; for el in elements {
217 if el.id() == exclude_id {
218 continue;
219 }
220 let pts = connection_points(el);
221 for cp in &pts {
222 let dist = ((cp.x - wx).powi(2) + (cp.y - wy).powi(2)).sqrt();
223 if dist < threshold && best.as_ref().is_none_or(|b| dist < b.3) {
224 best = Some((el.id().to_string(), cp.x, cp.y, dist));
225 }
226 }
227 }
228
229 best.map(|(id, x, y, _)| (id, x, y))
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn normalize_positive_bounds() {
238 let (x, y, w, h) = normalize_bounds(10.0, 20.0, 100.0, 50.0);
239 assert_eq!((x, y, w, h), (10.0, 20.0, 100.0, 50.0));
240 }
241
242 #[test]
243 fn normalize_negative_width() {
244 let (x, y, w, h) = normalize_bounds(110.0, 20.0, -100.0, 50.0);
245 assert_eq!((x, y, w, h), (10.0, 20.0, 100.0, 50.0));
246 }
247
248 #[test]
249 fn normalize_negative_both() {
250 let (x, y, w, h) = normalize_bounds(110.0, 70.0, -100.0, -50.0);
251 assert_eq!((x, y, w, h), (10.0, 20.0, 100.0, 50.0));
252 }
253
254 #[test]
255 fn arrowhead_horizontal_right() {
256 let pts = compute_arrowhead(100.0, 0.0, 0.0, 0.0, 14.0, 0.45);
257 assert!((pts.tip_x - 100.0).abs() < 1e-10);
258 assert!((pts.tip_y - 0.0).abs() < 1e-10);
259 assert!((pts.left_y + pts.right_y).abs() < 1e-10);
261 }
262
263 #[test]
264 fn hachure_lines_count() {
265 let lines = generate_hachure_lines(50.0, 50.0, 100.0, 100.0, 10.0, 0.0);
266 assert!(!lines.is_empty());
267 assert!(lines.len() > 20);
269 assert!(lines.len() < 40);
270 }
271
272 #[test]
273 fn hachure_lines_empty_for_huge_gap() {
274 let lines = generate_hachure_lines(50.0, 50.0, 10.0, 10.0, 1000.0, 0.0);
275 assert!(lines.len() <= 1);
277 }
278
279 #[test]
280 fn rectangle_connection_points_count() {
281 use crate::element::{Element, ShapeElement};
282 let el = Element::Rectangle(ShapeElement::new("r1".into(), 0.0, 0.0, 100.0, 50.0));
283 let pts = connection_points(&el);
284 assert_eq!(pts.len(), 8);
285 assert!((pts[0].x - 50.0).abs() < 1e-10);
287 assert!((pts[0].y - 0.0).abs() < 1e-10);
288 }
289
290 #[test]
291 fn ellipse_connection_points_count() {
292 use crate::element::{Element, ShapeElement};
293 let el = Element::Ellipse(ShapeElement::new("e1".into(), 0.0, 0.0, 100.0, 60.0));
294 let pts = connection_points(&el);
295 assert_eq!(pts.len(), 8);
296 assert!((pts[0].x - 50.0).abs() < 1e-10);
298 assert!((pts[0].y - 0.0).abs() < 1e-10);
299 }
300
301 #[test]
302 fn diamond_connection_points_count() {
303 use crate::element::{Element, ShapeElement};
304 let el = Element::Diamond(ShapeElement::new("d1".into(), 0.0, 0.0, 100.0, 80.0));
305 let pts = connection_points(&el);
306 assert_eq!(pts.len(), 8);
307 assert!((pts[0].x - 50.0).abs() < 1e-10);
309 assert!((pts[0].y - 0.0).abs() < 1e-10);
310 }
311
312 #[test]
313 fn find_snap_point_within_threshold() {
314 use crate::element::{Element, ShapeElement};
315 let elements = vec![Element::Rectangle(ShapeElement::new(
316 "r1".into(),
317 100.0,
318 100.0,
319 80.0,
320 60.0,
321 ))];
322 let result = find_nearest_snap_point(&elements, 142.0, 102.0, 15.0, "");
324 assert!(result.is_some());
325 let (id, sx, sy) = result.unwrap();
326 assert_eq!(id, "r1");
327 assert!((sx - 140.0).abs() < 1e-10);
328 assert!((sy - 100.0).abs() < 1e-10);
329 }
330
331 #[test]
332 fn find_snap_point_outside_threshold() {
333 use crate::element::{Element, ShapeElement};
334 let elements = vec![Element::Rectangle(ShapeElement::new(
335 "r1".into(),
336 100.0,
337 100.0,
338 80.0,
339 60.0,
340 ))];
341 let result = find_nearest_snap_point(&elements, 0.0, 0.0, 15.0, "");
343 assert!(result.is_none());
344 }
345
346 #[test]
347 fn find_snap_excludes_self() {
348 use crate::element::{Element, ShapeElement};
349 let elements = vec![Element::Rectangle(ShapeElement::new(
350 "r1".into(),
351 100.0,
352 100.0,
353 80.0,
354 60.0,
355 ))];
356 let result = find_nearest_snap_point(&elements, 140.0, 100.0, 15.0, "r1");
358 assert!(result.is_none());
359 }
360}