1use crate::doc::{EdgeArrow, EdgeRouting};
7
8pub const KINK_PX: f64 = 10.0;
10pub const TURN_RADIUS: f64 = 36.0;
12
13const DEFAULT_COLOR: &str = "#222";
14const DEFAULT_WIDTH: f64 = 1.6;
15const HEAD: f64 = 9.0;
16const DASH: &str = "6 4";
17const EQ_SEP: f64 = 3.2;
18pub const ANCHOR_GAP: f64 = 6.0;
20
21pub fn clip_box_edge(
23 cx: f64,
24 cy: f64,
25 tx: f64,
26 ty: f64,
27 w: f64,
28 h: f64,
29 pad: f64,
30) -> Pt {
31 let dx = tx - cx;
32 let dy = ty - cy;
33 if dx.abs() < 1e-9 && dy.abs() < 1e-9 {
34 return (cx, cy);
35 }
36 let hw = w * 0.5 + pad;
37 let hh = h * 0.5 + pad;
38 let sx = if dx.abs() > 1e-9 {
39 hw / dx.abs()
40 } else {
41 f64::INFINITY
42 };
43 let sy = if dy.abs() > 1e-9 {
44 hh / dy.abs()
45 } else {
46 f64::INFINITY
47 };
48 let t = sx.min(sy);
49 (cx + dx * t, cy + dy * t)
50}
51
52pub fn edge_anchors(
56 src: (f64, f64, f64, f64),
57 tgt: (f64, f64, f64, f64),
58 pad: f64,
59) -> (Pt, Pt) {
60 let (sx, sy, sw, sh) = src;
61 let (tx, ty, tw, th) = tgt;
62 let scx = sx + sw * 0.5;
63 let scy = sy + sh * 0.5;
64 let tcx = tx + tw * 0.5;
65 let tcy = ty + th * 0.5;
66 let p0 = clip_box_edge(scx, scy, tcx, tcy, sw, sh, pad);
67 let p1 = clip_box_edge(tcx, tcy, scx, scy, tw, th, pad);
68 (p0, p1)
69}
70
71pub type Pt = (f64, f64);
73
74fn hypot(dx: f64, dy: f64) -> f64 {
75 dx.hypot(dy)
76}
77
78fn point_line_distance(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
79 let dx = bx - ax;
80 let dy = by - ay;
81 let l2 = dx * dx + dy * dy;
82 if l2 < 1e-12 {
83 return hypot(px - ax, py - ay);
84 }
85 let t = ((px - ax) * dx + (py - ay) * dy) / l2;
86 let qx = ax + t * dx;
87 let qy = ay + t * dy;
88 hypot(px - qx, py - qy)
89}
90
91pub fn simplify_route(pts: &[Pt], kink_px: f64) -> Vec<Pt> {
93 if pts.len() < 2 {
94 return pts.to_vec();
95 }
96 let mut cleaned: Vec<Pt> = Vec::with_capacity(pts.len());
97 for &(x, y) in pts {
98 if cleaned
99 .last()
100 .map(|&(lx, ly)| hypot(x - lx, y - ly) > 1e-6)
101 .unwrap_or(true)
102 {
103 cleaned.push((x, y));
104 }
105 }
106 if cleaned.len() <= 2 {
107 return cleaned;
108 }
109
110 let mut changed = true;
112 while changed && cleaned.len() > 2 {
113 changed = false;
114 let mut out: Vec<Pt> = vec![cleaned[0]];
115 let mut i = 1;
116 while i + 1 < cleaned.len() {
117 let (x0, y0) = *out.last().unwrap();
118 let (x1, y1) = cleaned[i];
119 let (x2, y2) = cleaned[i + 1];
120 let seg = hypot(x1 - x0, y1 - y0);
121 let nxt = hypot(x2 - x1, y2 - y1);
122 if seg < kink_px || nxt < kink_px {
123 i += 1;
124 changed = true;
125 continue;
126 }
127 out.push(cleaned[i]);
128 i += 1;
129 }
130 out.push(*cleaned.last().unwrap());
131 cleaned = out;
132 }
133
134 changed = true;
136 while changed && cleaned.len() > 2 {
137 changed = false;
138 let mut out: Vec<Pt> = vec![cleaned[0]];
139 for i in 1..cleaned.len() - 1 {
140 let (ax, ay) = *out.last().unwrap();
141 let (px, py) = cleaned[i];
142 let (bx, by) = cleaned[i + 1];
143 if point_line_distance(px, py, ax, ay, bx, by) < kink_px {
144 changed = true;
145 continue;
146 }
147 out.push(cleaned[i]);
148 }
149 out.push(*cleaned.last().unwrap());
150 cleaned = out;
151 }
152 cleaned
153}
154
155pub fn axis_aligned(pts: &[Pt], tol: f64) -> bool {
157 if pts.len() < 2 {
158 return true;
159 }
160 for i in 1..pts.len() {
161 let dx = (pts[i].0 - pts[i - 1].0).abs();
162 let dy = (pts[i].1 - pts[i - 1].1).abs();
163 if dx > tol && dy > tol {
164 return false;
165 }
166 }
167 true
168}
169
170pub fn should_fillet(pts: &[Pt], routing: Option<EdgeRouting>) -> bool {
172 match routing {
173 Some(EdgeRouting::Orthogonal) => false,
174 Some(EdgeRouting::Polyline | EdgeRouting::Splines) => !axis_aligned(pts, 1e-3),
175 None => pts.len() > 2 && !axis_aligned(pts, 1e-3),
176 }
177}
178
179fn polyline_d(pts: &[Pt]) -> String {
180 if pts.is_empty() {
181 return String::new();
182 }
183 let mut bits = Vec::with_capacity(pts.len());
184 bits.push(format!("M {:.2} {:.2}", pts[0].0, pts[0].1));
185 for &(x, y) in &pts[1..] {
186 bits.push(format!("L {x:.2} {y:.2}"));
187 }
188 bits.join(" ")
189}
190
191pub fn filleted_path_d(pts: &[Pt], radius: f64) -> String {
193 if pts.len() < 2 {
194 return String::new();
195 }
196 if pts.len() == 2 || radius <= 0.0 {
197 return polyline_d(pts);
198 }
199 let mut bits: Vec<String> = vec![format!("M {:.2} {:.2}", pts[0].0, pts[0].1)];
200 for i in 1..pts.len() - 1 {
201 let (ax, ay) = pts[i - 1];
202 let (bx, by) = pts[i];
203 let (cx, cy) = pts[i + 1];
204 let v1x = ax - bx;
205 let v1y = ay - by;
206 let v2x = cx - bx;
207 let v2y = cy - by;
208 let len1 = hypot(v1x, v1y).max(1e-9);
209 let len2 = hypot(v2x, v2y).max(1e-9);
210 let dot = (v1x * v2x + v1y * v2y) / (len1 * len2);
211 if dot < -0.98 {
212 bits.push(format!("L {bx:.2} {by:.2}"));
213 continue;
214 }
215 let r = radius.min(0.45 * len1).min(0.45 * len2);
216 if r < 1.0 {
217 bits.push(format!("L {bx:.2} {by:.2}"));
218 continue;
219 }
220 let u1x = v1x / len1;
221 let u1y = v1y / len1;
222 let u2x = v2x / len2;
223 let u2y = v2y / len2;
224 let p1x = bx + u1x * r;
225 let p1y = by + u1y * r;
226 let p2x = bx + u2x * r;
227 let p2y = by + u2y * r;
228 bits.push(format!("L {p1x:.2} {p1y:.2}"));
229 bits.push(format!("Q {bx:.2} {by:.2} {p2x:.2} {p2y:.2}"));
230 }
231 let (lx, ly) = *pts.last().unwrap();
232 bits.push(format!("L {lx:.2} {ly:.2}"));
233 bits.join(" ")
234}
235
236pub fn shaft_path_d(pts: &[Pt], routing: Option<EdgeRouting>) -> String {
238 if should_fillet(pts, routing) {
239 filleted_path_d(pts, TURN_RADIUS)
240 } else {
241 polyline_d(pts)
242 }
243}
244
245fn unit(dx: f64, dy: f64) -> (f64, f64) {
246 let l = hypot(dx, dy).max(1e-9);
247 (dx / l, dy / l)
248}
249
250fn perp(ux: f64, uy: f64) -> (f64, f64) {
251 (-uy, ux)
252}
253
254fn polyline_length(pts: &[Pt]) -> f64 {
255 let mut total = 0.0;
256 for i in 1..pts.len() {
257 total += hypot(pts[i].0 - pts[i - 1].0, pts[i].1 - pts[i - 1].1);
258 }
259 total
260}
261
262fn shorten_polyline_end(pts: &[Pt], amount: f64) -> (Vec<Pt>, Pt, (f64, f64)) {
263 if pts.len() < 2 {
264 let p = pts.first().copied().unwrap_or((0.0, 0.0));
265 return (vec![p], p, (1.0, 0.0));
266 }
267 let tip = *pts.last().unwrap();
268 let mut pts = pts.to_vec();
269 let mut remaining = amount;
270 while pts.len() >= 2 && remaining > 0.0 {
271 let (x1, y1) = pts[pts.len() - 2];
272 let (x2, y2) = pts[pts.len() - 1];
273 let seg = hypot(x2 - x1, y2 - y1);
274 if seg <= 1e-9 {
275 pts.pop();
276 continue;
277 }
278 if seg > remaining {
279 let (ux, uy) = unit(x2 - x1, y2 - y1);
280 let nx = x2 - ux * remaining;
281 let ny = y2 - uy * remaining;
282 let last = pts.len() - 1;
283 pts[last] = (nx, ny);
284 return (pts, tip, (ux, uy));
285 }
286 remaining -= seg;
287 pts.pop();
288 }
289 let (ux, uy) = if pts.len() >= 2 {
290 unit(
291 pts[pts.len() - 1].0 - pts[pts.len() - 2].0,
292 pts[pts.len() - 1].1 - pts[pts.len() - 2].1,
293 )
294 } else {
295 (1.0, 0.0)
296 };
297 (pts, tip, (ux, uy))
298}
299
300fn filled_arrow_head_d(tip_x: f64, tip_y: f64, ux: f64, uy: f64, size: f64) -> String {
301 let (px, py) = perp(ux, uy);
302 let bx = tip_x - ux * size;
303 let by = tip_y - uy * size;
304 let half = size * 0.55;
305 format!(
306 "M {:.2} {:.2} L {:.2} {:.2} L {:.2} {:.2} Z",
307 bx + px * half,
308 by + py * half,
309 tip_x,
310 tip_y,
311 bx - px * half,
312 by - py * half
313 )
314}
315
316#[derive(Debug, Clone)]
318pub struct EdgePaintIn {
319 pub pts: Vec<Pt>,
320 pub arrow: EdgeArrow,
321 pub routing: Option<EdgeRouting>,
322 pub color: Option<String>,
323 pub stroke_width: Option<f64>,
324 pub dashed: bool,
325 pub label: Option<String>,
326 pub label_pos: Option<String>,
327 pub index: usize,
328}
329
330#[derive(Debug, Clone, PartialEq)]
332pub enum EdgePrim {
333 Path {
334 d: String,
335 stroke: String,
336 fill: String,
337 stroke_width: f64,
338 stroke_dasharray: Option<String>,
339 class: String,
340 },
341 Text {
342 x: f64,
343 y: f64,
344 text: String,
345 fill: String,
346 font_size: f64,
347 anchor: String,
348 class: String,
349 },
350}
351
352fn path_prim(
353 d: String,
354 stroke: &str,
355 fill: &str,
356 width: f64,
357 dashed: bool,
358 class: &str,
359) -> EdgePrim {
360 EdgePrim::Path {
361 d,
362 stroke: stroke.to_string(),
363 fill: fill.to_string(),
364 stroke_width: width,
365 stroke_dasharray: if dashed {
366 Some(DASH.to_string())
367 } else {
368 None
369 },
370 class: class.to_string(),
371 }
372}
373
374fn label_point(pts: &[Pt]) -> (f64, f64, f64, f64) {
375 if pts.len() < 2 {
376 let (x, y) = pts.first().copied().unwrap_or((0.0, 0.0));
377 return (x, y, 0.0, -1.0);
378 }
379 let mut best_i = 1;
380 let mut best_len = -1.0;
381 for i in 1..pts.len() {
382 let l = hypot(pts[i].0 - pts[i - 1].0, pts[i].1 - pts[i - 1].1);
383 if l > best_len {
384 best_len = l;
385 best_i = i;
386 }
387 }
388 let (x1, y1) = pts[best_i - 1];
389 let (x2, y2) = pts[best_i];
390 let (ux, uy) = unit(x2 - x1, y2 - y1);
391 let (px, py) = perp(ux, uy);
392 ((x1 + x2) * 0.5, (y1 + y2) * 0.5, px, py)
393}
394
395fn offset_polyline(pts: &[Pt], dist: f64) -> Vec<Pt> {
396 if pts.len() < 2 {
397 return pts.to_vec();
398 }
399 let n = pts.len();
400 let mut out = Vec::with_capacity(n);
401 for (i, &(x, y)) in pts.iter().enumerate() {
402 let (px, py) = if i == 0 {
403 let (ux, uy) = unit(pts[1].0 - x, pts[1].1 - y);
404 perp(ux, uy)
405 } else if i + 1 == n {
406 let (ux, uy) = unit(x - pts[i - 1].0, y - pts[i - 1].1);
407 perp(ux, uy)
408 } else {
409 let (u1x, u1y) = unit(x - pts[i - 1].0, y - pts[i - 1].1);
410 let (u2x, u2y) = unit(pts[i + 1].0 - x, pts[i + 1].1 - y);
411 let (p1x, p1y) = perp(u1x, u1y);
412 let (p2x, p2y) = perp(u2x, u2y);
413 let mut px = p1x + p2x;
414 let mut py = p1y + p2y;
415 let l = hypot(px, py).max(1e-9);
416 px /= l;
417 py /= l;
418 (px, py)
419 };
420 out.push((x + px * dist, y + py * dist));
421 }
422 out
423}
424
425fn harpoon_poly(
426 pts: &[Pt],
427 color: &str,
428 width: f64,
429 dashed: bool,
430 head_size: f64,
431 class: &str,
432 routing: Option<EdgeRouting>,
433) -> Vec<EdgePrim> {
434 let (shaft, tip, (ux, uy)) = shorten_polyline_end(pts, head_size);
435 let (px, py) = perp(ux, uy);
436 let barb = head_size * 0.55;
437 let bx = tip.0 - ux * head_size;
438 let by = tip.1 - uy * head_size;
439 let mut out = Vec::new();
440 if shaft.len() >= 2 {
441 out.push(path_prim(
442 shaft_path_d(&shaft, routing),
443 color,
444 "none",
445 width,
446 dashed,
447 class,
448 ));
449 }
450 out.push(path_prim(
451 format!(
452 "M {:.2} {:.2} L {:.2} {:.2}",
453 tip.0,
454 tip.1,
455 bx + px * barb,
456 by + py * barb
457 ),
458 color,
459 "none",
460 width,
461 false,
462 &format!("{class} harpoon"),
463 ));
464 out
465}
466
467pub fn edge_primitives(edge: &EdgePaintIn) -> Vec<EdgePrim> {
469 let pts = simplify_route(&edge.pts, KINK_PX);
470 if polyline_length(&pts) < 4.0 {
471 return Vec::new();
472 }
473 let color = edge.color.as_deref().unwrap_or(DEFAULT_COLOR);
474 let width = edge.stroke_width.unwrap_or(DEFAULT_WIDTH);
475 let dashed = edge.dashed;
476 let routing = edge.routing;
477 let cls = format!("edge edge-{}", edge.index);
478 let mut out: Vec<EdgePrim> = Vec::new();
479
480 match edge.arrow {
481 EdgeArrow::Equilibrium => {
482 let fwd = offset_polyline(&pts, EQ_SEP);
483 let mut rev = offset_polyline(&pts, -EQ_SEP);
484 rev.reverse();
485 out.extend(harpoon_poly(
486 &fwd,
487 color,
488 width,
489 dashed,
490 HEAD * 0.85,
491 &format!("{cls} eq-fwd"),
492 routing,
493 ));
494 out.extend(harpoon_poly(
495 &rev,
496 color,
497 width,
498 dashed,
499 HEAD * 0.85,
500 &format!("{cls} eq-rev"),
501 routing,
502 ));
503 }
504 EdgeArrow::Line => {
505 out.push(path_prim(
506 shaft_path_d(&pts, routing),
507 color,
508 "none",
509 width,
510 dashed,
511 &cls,
512 ));
513 }
514 EdgeArrow::Open => {
515 let (shaft, tip, (ux, uy)) = shorten_polyline_end(&pts, HEAD);
516 if shaft.len() >= 2 {
517 out.push(path_prim(
518 shaft_path_d(&shaft, routing),
519 color,
520 "none",
521 width,
522 dashed,
523 &cls,
524 ));
525 }
526 let (px, py) = perp(ux, uy);
527 let bx = tip.0 - ux * HEAD;
528 let by = tip.1 - uy * HEAD;
529 let half = HEAD * 0.55;
530 out.push(path_prim(
531 format!(
532 "M {:.2} {:.2} L {:.2} {:.2} L {:.2} {:.2}",
533 bx + px * half,
534 by + py * half,
535 tip.0,
536 tip.1,
537 bx - px * half,
538 by - py * half
539 ),
540 color,
541 "none",
542 width,
543 false,
544 &format!("{cls} head"),
545 ));
546 }
547 EdgeArrow::Forward => {
548 let (shaft, tip, (ux, uy)) = shorten_polyline_end(&pts, HEAD);
549 if shaft.len() >= 2 {
550 out.push(path_prim(
551 shaft_path_d(&shaft, routing),
552 color,
553 "none",
554 width,
555 dashed,
556 &cls,
557 ));
558 }
559 out.push(path_prim(
560 filled_arrow_head_d(tip.0, tip.1, ux, uy, HEAD),
561 "none",
562 color,
563 0.0,
564 false,
565 &format!("{cls} head"),
566 ));
567 }
568 }
569
570 if let Some(ref label) = edge.label {
571 let (mx, my, mut px, mut py) = label_point(&pts);
572 let pos = edge
573 .label_pos
574 .as_deref()
575 .unwrap_or("above")
576 .to_ascii_lowercase();
577 if pos == "below" || pos == "right" {
578 px = -px;
579 py = -py;
580 }
581 out.push(EdgePrim::Text {
582 x: mx + px * 10.0,
583 y: my + py * 10.0 + 4.0,
584 text: label.clone(),
585 fill: color.to_string(),
586 font_size: 11.0,
587 anchor: "middle".into(),
588 class: format!("{cls} label"),
589 });
590 }
591 out
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn paint(
599 pts: Vec<Pt>,
600 arrow: EdgeArrow,
601 routing: Option<EdgeRouting>,
602 ) -> EdgePaintIn {
603 EdgePaintIn {
604 pts,
605 arrow,
606 routing,
607 color: Some("#06c".into()),
608 stroke_width: Some(2.0),
609 dashed: true,
610 label: Some("lab".into()),
611 label_pos: Some("below".into()),
612 index: 1,
613 }
614 }
615
616 #[test]
617 fn simplify_snaps_micro_kink() {
618 let jog = vec![(0.0, 0.0), (100.0, 0.0), (100.0, 5.0), (200.0, 5.0)];
619 let flat = simplify_route(&jog, KINK_PX);
620 assert_eq!(flat.len(), 2);
621 assert_eq!(flat[0], (0.0, 0.0));
622 assert_eq!(flat[1], (200.0, 5.0));
623 }
624
625 #[test]
626 fn simplify_handles_short_and_duplicates() {
627 assert!(simplify_route(&[], KINK_PX).is_empty());
628 assert_eq!(simplify_route(&[(1.0, 2.0)], KINK_PX), vec![(1.0, 2.0)]);
629 let dups = vec![(0.0, 0.0), (0.0, 0.0), (10.0, 0.0)];
630 assert_eq!(simplify_route(&dups, KINK_PX).len(), 2);
631 let elbow = vec![(0.0, 0.0), (80.0, 0.0), (80.0, 60.0), (140.0, 60.0)];
633 assert_eq!(simplify_route(&elbow, KINK_PX).len(), 4);
634 }
635
636 #[test]
637 fn orthogonal_keeps_sharp_corners() {
638 let pts = vec![(0.0, 0.0), (80.0, 0.0), (80.0, 60.0), (140.0, 60.0)];
639 assert!(!should_fillet(&pts, Some(EdgeRouting::Orthogonal)));
640 assert!(!should_fillet(&pts, Some(EdgeRouting::Polyline))); assert!(!should_fillet(&pts, None));
642 let d = shaft_path_d(&pts, Some(EdgeRouting::Orthogonal));
643 assert!(!d.contains('Q'));
644 assert!(d.matches('L').count() >= 2);
645 }
646
647 #[test]
648 fn polyline_diagonal_gets_fillet() {
649 let pts = vec![(0.0, 0.0), (80.0, 0.0), (120.0, 60.0), (180.0, 60.0)];
650 assert!(should_fillet(&pts, Some(EdgeRouting::Polyline)));
651 assert!(should_fillet(&pts, Some(EdgeRouting::Splines)));
652 assert!(should_fillet(&pts, None));
653 let d = shaft_path_d(&pts, Some(EdgeRouting::Polyline));
654 assert!(d.contains('Q'));
655 }
656
657 #[test]
658 fn filleted_path_edge_cases() {
659 assert!(filleted_path_d(&[], TURN_RADIUS).is_empty());
660 assert!(!filleted_path_d(&[(0.0, 0.0), (10.0, 0.0)], TURN_RADIUS).contains('Q'));
661 assert!(!filleted_path_d(&[(0.0, 0.0), (10.0, 0.0), (20.0, 0.0)], 0.0).contains('Q'));
662 let almost = vec![(0.0, 0.0), (50.0, 0.0), (100.0, 0.1)];
664 let d = filleted_path_d(&almost, TURN_RADIUS);
665 assert!(d.contains('L'));
666 let tiny = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)];
668 assert!(!filleted_path_d(&tiny, TURN_RADIUS).contains('Q'));
669 }
670
671 #[test]
672 fn axis_aligned_and_point_line() {
673 assert!(axis_aligned(&[(0.0, 0.0)], 1e-3));
674 assert!(axis_aligned(&[(0.0, 0.0), (1.0, 0.0), (1.0, 2.0)], 1e-3));
675 assert!(!axis_aligned(&[(0.0, 0.0), (1.0, 1.0)], 1e-3));
676 assert!(point_line_distance(0.0, 1.0, 0.0, 0.0, 0.0, 0.0) >= 0.0);
677 assert!((point_line_distance(1.0, 1.0, 0.0, 0.0, 2.0, 0.0) - 1.0).abs() < 1e-9);
678 }
679
680 #[test]
681 fn shorten_polyline_edge_cases() {
682 let (s, tip, _) = shorten_polyline_end(&[], 5.0);
683 assert_eq!(s.len(), 1);
684 assert_eq!(tip, (0.0, 0.0));
685 let (s, tip, dir) = shorten_polyline_end(&[(1.0, 2.0)], 5.0);
686 assert_eq!(tip, (1.0, 2.0));
687 assert_eq!(dir, (1.0, 0.0));
688 assert_eq!(s, vec![(1.0, 2.0)]);
689 let pts = vec![(0.0, 0.0), (0.0, 0.0), (100.0, 0.0)];
691 let (s, _, (ux, _)) = shorten_polyline_end(&pts, 10.0);
692 assert!(s.len() >= 2);
693 assert!(ux > 0.0);
694 let (s, _, _) = shorten_polyline_end(&[(0.0, 0.0), (5.0, 0.0)], 20.0);
696 assert!(s.len() <= 2);
697 }
698
699 #[test]
700 fn forward_edge_emits_shaft_and_head() {
701 let edge = paint(vec![(0.0, 0.0), (100.0, 0.0)], EdgeArrow::Forward, None);
702 let prims = edge_primitives(&edge);
703 assert!(prims.len() >= 2);
704 assert!(prims.iter().any(|p| matches!(p, EdgePrim::Text { .. })));
705 assert!(prims.iter().any(|p| matches!(
706 p,
707 EdgePrim::Path {
708 stroke_dasharray: Some(_),
709 ..
710 }
711 )));
712 }
713
714 #[test]
715 fn line_open_equilibrium_arrows() {
716 let pts = vec![(0.0, 0.0), (40.0, 0.0), (40.0, 40.0), (80.0, 40.0)];
717 for arrow in [EdgeArrow::Line, EdgeArrow::Open, EdgeArrow::Equilibrium] {
718 let mut edge = paint(pts.clone(), arrow, Some(EdgeRouting::Orthogonal));
719 edge.label_pos = Some("above".into());
720 let prims = edge_primitives(&edge);
721 assert!(!prims.is_empty(), "{arrow:?}");
722 }
723 let short = paint(vec![(0.0, 0.0), (1.0, 0.0)], EdgeArrow::Line, None);
725 assert!(edge_primitives(&short).is_empty());
726 }
727
728 #[test]
729 fn edge_anchors_on_box_boundary() {
730 let (p0, p1) = edge_anchors((0.0, 0.0, 100.0, 80.0), (200.0, 0.0, 100.0, 80.0), 0.0);
731 assert!((p0.0 - 100.0).abs() < 1e-6);
732 assert!((p1.0 - 200.0).abs() < 1e-6);
733 assert!((p0.1 - 40.0).abs() < 1e-6);
734 let same = clip_box_edge(0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 0.0);
735 assert_eq!(same, (0.0, 0.0));
736 }
737
738 #[test]
739 fn offset_and_label_helpers() {
740 assert_eq!(offset_polyline(&[(0.0, 0.0)], 2.0).len(), 1);
741 let off = offset_polyline(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)], 2.0);
742 assert_eq!(off.len(), 3);
743 let (x, y, _, _) = label_point(&[]);
744 assert_eq!((x, y), (0.0, 0.0));
745 let _ = label_point(&[(5.0, 5.0)]);
746 }
747}