svgbob/buffer/fragment_buffer/fragment/
line.rs

1use crate::{
2    buffer::{fragment_buffer::fragment::polygon::Polygon, Cell, Fragment},
3    fragment::{marker_line, Bounds, Circle, Marker, MarkerLine},
4    util, Direction, Point,
5};
6use parry2d::{
7    bounding_volume::Aabb,
8    math::Isometry,
9    query::PointQuery,
10    shape::{Polyline, Segment, Shape},
11};
12use std::{cmp::Ordering, fmt};
13
14use crate::fragment::Arc;
15use sauron::{html::attributes::*, svg, svg::attributes::*, Node};
16
17#[derive(Debug, Clone)]
18pub struct Line {
19    pub start: Point,
20    pub end: Point,
21    pub is_broken: bool,
22}
23
24impl Line {
25    /// creates a new line and reorder the points swapping the end points if necessary
26    /// such that the start is the most top-left and end point is the most bottom-right
27    pub fn new(start: Point, end: Point, is_broken: bool) -> Self {
28        let mut line = Line {
29            start,
30            end,
31            is_broken,
32        };
33        line.sort_reorder_end_points();
34        line
35    }
36
37    /// creates a new line, but don't reorder the points
38    pub(crate) fn new_noswap(
39        start: Point,
40        end: Point,
41        is_broken: bool,
42    ) -> Self {
43        Line {
44            start,
45            end,
46            is_broken,
47        }
48    }
49
50    /// reorder the end points swap end points such that
51    /// start < end
52    pub(crate) fn sort_reorder_end_points(&mut self) {
53        if self.start > self.end {
54            self.swap()
55        }
56    }
57
58    fn swap(&mut self) {
59        std::mem::swap(&mut self.start, &mut self.end);
60    }
61
62    /// does this line can completely cover line a b?
63    pub(crate) fn overlaps(&self, a: Point, b: Point) -> bool {
64        let segment = Segment::new(*self.start, *self.end);
65        let identity = &Isometry::identity();
66        segment.contains_point(identity, &a)
67            && segment.contains_point(identity, &b)
68    }
69
70    fn contains_point(&self, p: Point) -> bool {
71        let segment = Segment::new(*self.start, *self.end);
72        let identity = &Isometry::identity();
73        segment.contains_point(identity, &p)
74    }
75
76    fn touching_line(&self, other: &Self) -> bool {
77        self.contains_point(other.start) || self.contains_point(other.end)
78    }
79
80    fn octant(&self) -> u8 {
81        let mut dx = self.end.x - self.start.x;
82        let mut dy = -(self.end.y * 2.0 - self.start.y * 2.0);
83
84        let mut octant = 0;
85
86        if dy < 0.0 {
87            dx = -dx;
88            dy = -dy;
89            octant += 4;
90        }
91
92        if dx < 0.0 {
93            let tmp = dx;
94            dx = dy;
95            dy = -tmp;
96            octant += 2
97        }
98
99        if dx < dy {
100            octant += 1
101        }
102        octant
103    }
104
105    //phi = atan(m1) - atan(m2);
106    fn angle_rad(&self) -> f32 {
107        let m1 = self.slope();
108        0.0 - m1.atan()
109    }
110
111    fn angle_deg(&self) -> f32 {
112        self.angle_rad().to_degrees()
113    }
114
115    fn slope(&self) -> f32 {
116        (2.0 * self.end.y as f32 - 2.0 * self.start.y as f32)
117            / (self.end.x as f32 - self.start.x as f32)
118    }
119
120    fn full_angle(&self) -> f32 {
121        let angle = self.angle_deg().abs();
122        match self.octant() {
123            0..=1 => angle,
124            2..=3 => 180.0 - angle,
125            4..=5 => 180.0 + angle,
126            6..=7 => 360.0 - angle,
127            _ => angle,
128        }
129    }
130
131    /// round angle closest to
132    ///
133    /// slash line is not really 60% but 63.435
134    /// 63.435  0   63.435
135    ///         180 116.565
136    ///         180 243.435
137    ///         360 296.565
138    ///
139    ///
140    fn line_angle(&self) -> f32 {
141        let angle = self.full_angle().round() as i32;
142        match angle {
143            0..=10 => 0.0,
144            11..=50 => 63.435, //45.0,
145            51..=80 => 63.435,
146            81..=100 => 90.0,
147            101..=130 => 116.565,
148            131..=170 => 116.565, //135.0,
149            171..=190 => 180.0,
150            191..=230 => 243.435, //225.0,
151            231..=260 => 243.435,
152            261..=280 => 270.0,
153            281..=310 => 296.565,
154            311..=350 => 296.565, //315.0,
155            351..=360 => 0.0,
156            _ => 0.0,
157        }
158    }
159
160    /// 0   0
161    /// 45  45
162    /// 63.435  63
163    /// 90  90
164    /// 116.565 117
165    /// 135 135
166    /// 180 180
167    /// 225 225
168    /// 243.435 243
169    /// 270 270
170    /// 296.565 297
171    /// 315 315
172    /// 360 360
173    pub(crate) fn heading(&self) -> Direction {
174        match self.line_angle().round() as i32 {
175            0 => Direction::Right,
176            45 => Direction::TopRight,
177            63 => Direction::TopRight,
178            90 => Direction::Top,
179            117 => Direction::TopLeft,
180            135 => Direction::TopLeft,
181            180 => Direction::Left,
182            225 => Direction::BottomLeft,
183            243 => Direction::BottomLeft,
184            270 => Direction::Bottom,
185            297 => Direction::BottomRight,
186            315 => Direction::BottomRight,
187            _ => unreachable!(),
188        }
189    }
190
191    pub(crate) fn is_touching_circle(&self, circle: &Circle) -> bool {
192        let center = circle.center;
193        let distance_end_center = self.end.distance(&center);
194        let distance_start_center = self.start.distance(&center);
195
196        let _threshold_length = self.heading().threshold_length();
197        let is_close_start_point = distance_start_center < (circle.radius);
198        let is_close_end_point = distance_end_center < (circle.radius);
199        is_close_start_point || is_close_end_point
200    }
201
202    /// considering lines are sorted based on their start and end points
203    /// the line should be touching each other
204    /// and are collinear ( lies on the same line) can be test by computing the triangle area which
205    /// should be equal to 0.
206    /// therefore can merge
207    pub(crate) fn can_merge(&self, other: &Self) -> bool {
208        self.is_touching(other)
209            && util::is_collinear(&self.start, &self.end, &other.start)
210            && util::is_collinear(&self.start, &self.end, &other.end)
211    }
212
213    /// check if this line and the other can merge
214    /// returns None if it can not merge
215    /// the merged line used the starting_point of self and the end_point of other
216    pub(crate) fn merge(&self, other: &Self) -> Option<Self> {
217        if self.can_merge(other) {
218            let start = std::cmp::min(self.start, other.start);
219            let end = std::cmp::max(self.end, other.end);
220            // when one of them is broken line, then everything will be broken line
221            Some(Line::new(start, end, self.is_broken || other.is_broken))
222        } else {
223            None
224        }
225    }
226
227    pub(crate) fn merge_circle(&self, circle: &Circle) -> Option<Fragment> {
228        let distance_end_center = self.end.distance(&circle.center);
229        let distance_start_center = self.start.distance(&circle.center);
230
231        let threshold_length = self.heading().threshold_length();
232        let is_close_start_point =
233            distance_start_center <= threshold_length * 0.75;
234        let is_close_end_point = distance_end_center <= threshold_length * 0.75;
235
236        let can_merge = circle.radius <= Cell::unit(3)
237            && (is_close_start_point || is_close_end_point);
238
239        if can_merge {
240            let marker = if circle.is_filled {
241                Some(Marker::Circle)
242            } else if circle.radius >= Cell::unit(2) {
243                Some(Marker::BigOpenCircle)
244            } else {
245                Some(Marker::OpenCircle)
246            };
247            let new_line = if is_close_end_point {
248                Line::new_noswap(self.start, circle.center, self.is_broken)
249            } else if is_close_start_point {
250                // if close to the start, swap the end points of the line
251                Line::new_noswap(self.end, circle.center, self.is_broken)
252            } else {
253                panic!("There is no endpoint of the line is that close to the arrow");
254            };
255
256            let marker_line = marker_line(
257                new_line.start,
258                new_line.end,
259                new_line.is_broken,
260                None,
261                marker,
262            );
263            Some(marker_line)
264        } else {
265            None
266        }
267    }
268
269    /// check to see if any of the line endpoints is touching.
270    /// this will be used to group lines together
271    /// This does not check if the lines are intersecting
272    pub(crate) fn is_touching(&self, other: &Self) -> bool {
273        self.touching_line(other) || other.touching_line(self)
274    }
275
276    pub(crate) fn has_endpoint(&self, p: Point) -> bool {
277        self.start == p || self.end == p
278    }
279
280    pub(crate) fn is_touching_arc(&self, other: &Arc) -> bool {
281        self.start == other.start
282            || self.end == other.end
283            || self.start == other.end
284            || self.end == other.start
285    }
286
287    /// check if this a horizontal line
288    fn is_horizontal(&self) -> bool {
289        self.start.y == self.end.y
290    }
291
292    /// check if this is a vertical line
293    fn is_vertical(&self) -> bool {
294        self.start.x == self.end.x
295    }
296
297    /// check if 2 lines are parallel in axis align box
298    /// for the purpose of promoting lines into rect
299    pub(crate) fn is_aabb_parallel(&self, other: &Self) -> bool {
300        (self.is_horizontal()
301            && other.is_horizontal()
302            && self.start.x == other.start.x
303            && self.end.x == other.end.x)
304            || (self.is_vertical()
305                && other.is_vertical()
306                && self.start.y == other.start.y
307                && self.end.y == other.end.y)
308    }
309
310    /// check if 2 lines are perpendicular in axis align box only
311    /// for the purpose of promoting lines into rect
312    pub(crate) fn is_aabb_perpendicular(&self, other: &Self) -> bool {
313        (self.is_horizontal() && other.is_vertical())
314            || (self.is_vertical() && other.is_horizontal())
315    }
316
317    /// check if 2 lines are touching and aabb perpendicular at the same time
318    pub(crate) fn is_touching_aabb_perpendicular(&self, other: &Self) -> bool {
319        self.is_touching(other) && self.is_aabb_perpendicular(other)
320    }
321
322    /// recompute the line with start and end point offset by the cell
323    /// location
324    pub(crate) fn absolute_position(&self, cell: Cell) -> Self {
325        Line {
326            start: cell.absolute_position(self.start),
327            end: cell.absolute_position(self.end),
328            is_broken: self.is_broken,
329        }
330    }
331
332    pub fn scale(&self, scale: f32) -> Self {
333        Line {
334            start: self.start.scale(scale),
335            end: self.end.scale(scale),
336            is_broken: self.is_broken,
337        }
338    }
339
340    pub(crate) fn is_broken(&self) -> bool {
341        self.is_broken
342    }
343
344    pub fn localize(&self, cell: Cell) -> Self {
345        Line {
346            start: cell.localize_point(self.start),
347            end: cell.localize_point(self.end),
348            is_broken: self.is_broken,
349        }
350    }
351
352    pub(crate) fn align(&self) -> Self {
353        Line {
354            start: self.start.align(),
355            end: self.end.align(),
356            is_broken: self.is_broken,
357        }
358    }
359
360    /// extend the line by a length added to the end point
361    /// https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance#7741655
362    pub fn extend(&self, length: f32) -> Self {
363        let d = self.start.distance(&self.end);
364        let cx = self.end.x + (self.end.x - self.start.x) / d * length;
365        let cy = self.end.y + (self.end.y - self.start.y) / d * length;
366        Line::new_noswap(self.start, Point::new(cx, cy), self.is_broken)
367    }
368
369    /// extend but on the opposite direction
370    /// TODO: This implementation is hacky
371    pub fn extend_start(&self, length: f32) -> Self {
372        let mut tmp_line = self.clone();
373        tmp_line.swap();
374        let mut new_line = tmp_line.extend(length);
375        new_line.swap();
376        new_line
377    }
378}
379
380impl Bounds for Line {
381    fn bounds(&self) -> (Point, Point) {
382        let aabb = Segment::new(*self.start, *self.end).local_aabb();
383        (Point::from(*aabb.mins), Point::from(*aabb.maxs))
384    }
385}
386
387impl fmt::Display for Line {
388    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
389        write!(f, "L {} {} {}", self.start, self.end, self.is_broken)
390    }
391}
392
393impl<MSG> From<Line> for Node<MSG> {
394    fn from(line: Line) -> Node<MSG> {
395        svg::line(
396            [
397                x1(line.start.x),
398                y1(line.start.y),
399                x2(line.end.x),
400                y2(line.end.y),
401                classes_flag([
402                    ("broken", line.is_broken),
403                    ("solid", !line.is_broken),
404                ]),
405            ],
406            [],
407        )
408    }
409}
410
411impl From<Line> for Segment {
412    fn from(line: Line) -> Segment {
413        Segment::new(*line.start, *line.end)
414    }
415}
416
417impl Eq for Line {}
418
419/// This is needed since this struct contains f32 which rust doesn't provide Eq implementation
420impl Ord for Line {
421    fn cmp(&self, other: &Self) -> Ordering {
422        self.start
423            .cmp(&other.start)
424            .then(self.end.cmp(&other.end))
425            .then(self.is_broken.cmp(&other.is_broken))
426    }
427}
428
429impl PartialOrd for Line {
430    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
431        Some(self.cmp(other))
432    }
433}
434
435impl PartialEq for Line {
436    fn eq(&self, other: &Self) -> bool {
437        self.cmp(other) == Ordering::Equal
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::buffer::{
445        fragment_buffer::fragment::polygon::PolygonTag, CellGrid,
446    };
447
448    #[test]
449    fn test_extend_line() {
450        let line1 = Line::new_noswap(
451            Point::new(0.0, 0.0),
452            Point::new(10.0, 0.0),
453            false,
454        );
455        let extended = line1.extend(1.0);
456        assert_eq!(
457            extended,
458            Line::new(Point::new(0.0, 0.0), Point::new(11.0, 0.0), false)
459        );
460        let extended2 = line1.extend(2.0);
461        assert_eq!(
462            extended2,
463            Line::new(Point::new(0.0, 0.0), Point::new(12.0, 0.0), false)
464        );
465    }
466
467    #[test]
468    fn test_extend_line_start() {
469        let line1 = Line::new_noswap(
470            Point::new(0.0, 0.0),
471            Point::new(10.0, 0.0),
472            false,
473        );
474        let extended = line1.extend_start(1.0);
475        assert_eq!(
476            extended,
477            Line::new(Point::new(-1.0, 0.0), Point::new(10.0, 0.0), false)
478        );
479        let extended2 = line1.extend_start(2.0);
480        assert_eq!(
481            extended2,
482            Line::new(Point::new(-2.0, 0.0), Point::new(10.0, 0.0), false)
483        );
484    }
485
486    #[test]
487    fn test_extend_line_vertical() {
488        let line1 = Line::new_noswap(
489            Point::new(0.0, 0.0),
490            Point::new(0.0, 10.0),
491            false,
492        );
493        let extended = line1.extend(1.0);
494        assert_eq!(
495            extended,
496            Line::new(Point::new(0.0, 0.0), Point::new(0.0, 11.0), false)
497        );
498        let extended2 = line1.extend(2.0);
499        assert_eq!(
500            extended2,
501            Line::new(Point::new(0.0, 0.0), Point::new(0.0, 12.0), false)
502        );
503    }
504
505    #[test]
506    fn line_merge() {
507        let line1 =
508            Line::new(Point::new(4.0, 0.0), Point::new(2.0, 4.0), false);
509        let line2 =
510            Line::new(Point::new(2.0, 4.0), Point::new(1.0, 6.0), false);
511        assert!(line1.is_touching(&line2));
512        assert!(line2.is_touching(&line1));
513        assert!(util::is_collinear(&line1.start, &line1.end, &line2.start));
514        assert!(util::is_collinear(&line2.start, &line2.end, &line1.end));
515        assert!(line1.can_merge(&line2));
516    }
517
518    #[test]
519    fn test_angle() {
520        let m = CellGrid::m();
521        let k = CellGrid::k();
522        let c = CellGrid::c();
523        let o = CellGrid::o();
524        let e = CellGrid::e();
525        let a = CellGrid::a();
526        let y = CellGrid::y();
527        let u = CellGrid::u();
528
529        assert_eq!(0.0, 0.0f32.atan());
530
531        let line = Line::new(c, m, false);
532        assert_eq!(line.line_angle(), 270.0);
533
534        let line2 = Line::new(m, o, false);
535        assert_eq!(line2.line_angle(), 0.0);
536
537        let line3 = Line::new(a, y, false);
538        assert_eq!(line3.line_angle(), 296.565);
539
540        let line4 = Line::new(k, o, false);
541        assert_eq!(line4.line_angle(), 0.0);
542
543        let line6 = Line::new(u, e, false);
544        assert_eq!(line6.line_angle(), 243.435);
545
546        let line5 = Line::new(e, u, false);
547        assert_eq!(line5.line_angle(), 243.435);
548    }
549
550    #[test]
551    fn test_bounds() {
552        let d = CellGrid::d();
553        let e = CellGrid::e();
554        let line = Line::new(e, d, false);
555        assert_eq!(line.bounds(), (d, e));
556    }
557
558    #[test]
559    fn test_merge() {
560        let a = CellGrid::a();
561        let b = CellGrid::b();
562        let c = CellGrid::c();
563        let d = CellGrid::d();
564        assert!(Line::new(a, b, false).can_merge(&Line::new(b, c, false)));
565        assert!(Line::new(b, c, false).can_merge(&Line::new(b, c, false)));
566        assert!(Line::new(b, c, false).can_merge(&Line::new(c, b, false)));
567        assert!(Line::new(b, c, false).can_merge(&Line::new(c, d, false)));
568    }
569
570    #[test]
571    fn test_merge_kmo() {
572        let k = CellGrid::k();
573        let m = CellGrid::m();
574        let o = CellGrid::o();
575        assert!(Line::new(k, m, false).can_merge(&Line::new(m, o, false)));
576        assert_eq!(
577            Some(Line::new(k, o, false)),
578            Line::new(k, m, false).merge(&Line::new(m, o, false))
579        );
580    }
581
582    #[test]
583    fn test_merge_cmw() {
584        let c = CellGrid::c();
585        let m = CellGrid::m();
586        let w = CellGrid::w();
587        assert!(Line::new(c, m, false).can_merge(&Line::new(m, w, false)));
588        assert_eq!(
589            Some(Line::new(c, w, false)),
590            Line::new(c, m, false).merge(&Line::new(m, w, false))
591        );
592    }
593}