1use monospace_core::Shape as _;
14use monospace_core::{
15 BoxShape, Connector, Direction, Glyph, Line, Orientation, Pos, Size, Stroke, Surface, Terminal,
16 Text, TextShape,
17};
18
19use crate::position::{flat_size, side_centre};
20use crate::{Anchor, Delta, Diagram, Position, Reference, ShapeId};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum End {
25 From,
27 To,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum EndpointAt {
38 Absolute {
40 at: Pos,
42 leaving: Direction,
44 },
45 Reference(Reference),
47}
48
49impl EndpointAt {
50 #[must_use]
52 pub const fn position(self) -> Position {
53 match self {
54 Self::Absolute { at, .. } => Position::Absolute(at),
55 Self::Reference(reference) => Position::Reference(reference),
56 }
57 }
58
59 #[must_use]
61 pub const fn leaving(self) -> Direction {
62 match self {
63 Self::Absolute { leaving, .. } => leaving,
64 Self::Reference(reference) => reference.anchor.leaving(),
65 }
66 }
67
68 #[must_use]
70 pub(crate) fn resolve(self, diagram: &Diagram, terminal: &Terminal) -> Option<Pos> {
71 match self {
72 Self::Absolute { at, .. } => Some(at),
73 Self::Reference(reference) => {
74 let shape = diagram.get(reference.id)?;
75 let point = shape.anchor(reference.anchor)?;
76 Some(
77 reference
78 .offset
79 .grow(reference.anchor.outward(self.effective_out(terminal)))
80 .apply(point),
81 )
82 }
83 }
84 }
85
86 #[must_use]
92 pub(crate) fn effective_out(self, terminal: &Terminal) -> i16 {
93 match self {
94 Self::Absolute { .. } => 0,
95 Self::Reference(reference) => {
96 let base = i16::from(*terminal != Terminal::Arm);
97 reference.out.saturating_add(base)
98 }
99 }
100 }
101
102 #[must_use]
104 pub(crate) fn displaced_by(self, by: Delta) -> Self {
105 match self.position().displaced_by(by) {
106 Position::Absolute(at) => Self::Absolute {
107 at,
108 leaving: self.leaving(),
109 },
110 Position::Reference(reference) => Self::Reference(reference),
111 }
112 }
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct Endpoint {
129 pub at: EndpointAt,
131 pub terminal: Terminal,
133}
134
135impl Endpoint {
136 #[must_use]
138 pub const fn absolute(at: Pos, leaving: Direction, terminal: Terminal) -> Self {
139 Self {
140 at: EndpointAt::Absolute { at, leaving },
141 terminal,
142 }
143 }
144
145 #[must_use]
147 pub const fn reference(reference: Reference, terminal: Terminal) -> Self {
148 Self {
149 at: EndpointAt::Reference(reference),
150 terminal,
151 }
152 }
153
154 #[must_use]
156 pub const fn position(&self) -> Position {
157 self.at.position()
158 }
159
160 #[must_use]
162 pub const fn leaving(&self) -> Direction {
163 self.at.leaving()
164 }
165
166 #[must_use]
168 pub fn effective_out(&self) -> i16 {
169 self.at.effective_out(&self.terminal)
170 }
171
172 #[must_use]
175 pub fn resolve(&self, diagram: &Diagram) -> Option<Pos> {
176 self.at.resolve(diagram, &self.terminal)
177 }
178
179 pub(crate) fn frozen_position(&self, id: ShapeId, diagram: &Diagram) -> Option<EndpointAt> {
182 match self.at {
183 EndpointAt::Reference(reference) if reference.id == id => {
184 self.resolve(diagram).map(|at| EndpointAt::Absolute {
185 at,
186 leaving: reference.anchor.leaving(),
187 })
188 }
189 _ => None,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum Shape {
202 Box {
204 at: Pos,
206 size: Size,
208 stroke: Stroke,
210 fill: Option<Glyph>,
212 text: Option<Text>,
214 },
215 Line {
217 at: Pos,
219 len: u16,
221 orientation: Orientation,
223 stroke: Stroke,
225 },
226 Connector {
228 from: Endpoint,
230 to: Endpoint,
232 stroke: Stroke,
234 },
235}
236
237impl Shape {
238 pub(crate) fn anchor(&self, anchor: Anchor) -> Option<Pos> {
251 match self {
252 Self::Box { at, size, .. } => Some(side_centre(*at, *size, anchor)),
253 Self::Line {
254 at,
255 len,
256 orientation,
257 ..
258 } => Some(side_centre(*at, flat_size(*len, *orientation), anchor)),
259 Self::Connector { .. } => None,
260 }
261 }
262
263 pub(crate) fn draw(&self, surface: &mut impl Surface, diagram: &Diagram) {
276 match self {
277 Self::Box {
278 at,
279 size,
280 stroke,
281 fill,
282 text,
283 } => {
284 BoxShape {
285 at: *at,
286 size: *size,
287 stroke: stroke.clone(),
288 fill: fill.clone(),
289 }
290 .draw(surface);
291
292 if let Some(text) = text {
293 let (Some(x), Some(y)) = (at.x.checked_add(1), at.y.checked_add(1)) else {
294 return;
295 };
296 TextShape {
297 at: Pos { x, y },
298 area: Size {
299 width: size.width.saturating_sub(2),
300 height: size.height.saturating_sub(2),
301 },
302 text: text.clone(),
303 }
304 .draw(surface);
305 }
306 }
307 Self::Line {
308 at,
309 len,
310 orientation,
311 stroke,
312 } => Line {
313 at: *at,
314 len: *len,
315 orientation: *orientation,
316 stroke: stroke.clone(),
317 }
318 .draw(surface),
319 Self::Connector { from, to, stroke } => {
320 let (Some(from_at), Some(to_at)) = (from.resolve(diagram), to.resolve(diagram))
321 else {
322 return;
323 };
324 Connector {
325 from: monospace_core::Endpoint {
326 at: from_at,
327 leaving: from.at.leaving(),
328 terminal: from.terminal.clone(),
329 },
330 to: monospace_core::Endpoint {
331 at: to_at,
332 leaving: to.at.leaving(),
333 terminal: to.terminal.clone(),
334 },
335 stroke: stroke.clone(),
336 }
337 .draw(surface);
338 }
339 }
340 }
341
342 #[must_use]
368 pub fn displaced_by(&self, by: Delta) -> Self {
369 match self {
370 Self::Box {
371 at,
372 size,
373 stroke,
374 fill,
375 text,
376 } => Self::Box {
377 at: by.apply(*at),
378 size: *size,
379 stroke: stroke.clone(),
380 fill: fill.clone(),
381 text: text.clone(),
382 },
383 Self::Line {
384 at,
385 len,
386 orientation,
387 stroke,
388 } => Self::Line {
389 at: by.apply(*at),
390 len: *len,
391 orientation: *orientation,
392 stroke: stroke.clone(),
393 },
394 Self::Connector { from, to, stroke } => Self::Connector {
395 from: Endpoint {
396 at: from.at.displaced_by(by),
397 terminal: from.terminal.clone(),
398 },
399 to: Endpoint {
400 at: to.at.displaced_by(by),
401 terminal: to.terminal.clone(),
402 },
403 stroke: stroke.clone(),
404 },
405 }
406 }
407
408 #[must_use]
429 pub fn resized_by(&self, by: Delta) -> Self {
430 match self {
431 Self::Box {
432 at,
433 size,
434 stroke,
435 fill,
436 text,
437 } => Self::Box {
438 at: *at,
439 size: Size {
440 width: clamp_dimension(size.width, by.dx, 2),
441 height: clamp_dimension(size.height, by.dy, 2),
442 },
443 stroke: stroke.clone(),
444 fill: fill.clone(),
445 text: text.clone(),
446 },
447 Self::Line {
448 at,
449 len,
450 orientation,
451 stroke,
452 } => {
453 let delta = match orientation {
454 Orientation::Horizontal => by.dx,
455 Orientation::Vertical => by.dy,
456 };
457 Self::Line {
458 at: *at,
459 len: clamp_dimension(*len, delta, 1),
460 orientation: *orientation,
461 stroke: stroke.clone(),
462 }
463 }
464 Self::Connector { .. } => self.clone(),
465 }
466 }
467
468 #[must_use]
474 pub const fn can_resize(&self) -> bool {
475 matches!(self, Self::Box { .. } | Self::Line { .. })
476 }
477
478 #[must_use]
489 pub fn with_stroke(&self, stroke: Stroke) -> Self {
490 match self {
491 Self::Box {
492 at,
493 size,
494 fill,
495 text,
496 ..
497 } => Self::Box {
498 at: *at,
499 size: *size,
500 stroke,
501 fill: fill.clone(),
502 text: text.clone(),
503 },
504 Self::Line {
505 at,
506 len,
507 orientation,
508 ..
509 } => Self::Line {
510 at: *at,
511 len: *len,
512 orientation: *orientation,
513 stroke,
514 },
515 Self::Connector { from, to, .. } => Self::Connector {
516 from: from.clone(),
517 to: to.clone(),
518 stroke,
519 },
520 }
521 }
522
523 #[must_use]
534 pub fn with_text(&self, text: Option<Text>) -> Self {
535 match self {
536 Self::Box {
537 at,
538 size,
539 stroke,
540 fill,
541 ..
542 } => Self::Box {
543 at: *at,
544 size: *size,
545 stroke: stroke.clone(),
546 fill: fill.clone(),
547 text,
548 },
549 Self::Line { .. } | Self::Connector { .. } => self.clone(),
550 }
551 }
552
553 #[must_use]
567 pub fn with_rotated_orientation(&self) -> Self {
568 match self {
569 Self::Line {
570 at,
571 len,
572 orientation,
573 stroke,
574 } => {
575 let toggled = match orientation {
576 Orientation::Horizontal => Orientation::Vertical,
577 Orientation::Vertical => Orientation::Horizontal,
578 };
579 Self::Line {
580 at: *at,
581 len: *len,
582 orientation: toggled,
583 stroke: stroke.clone(),
584 }
585 }
586 Self::Box { .. } | Self::Connector { .. } => self.clone(),
587 }
588 }
589
590 #[must_use]
603 pub fn with_inverted_terminals(&self) -> Self {
604 match self {
605 Self::Connector { from, to, stroke } => Self::Connector {
606 from: Endpoint {
607 at: from.at,
608 terminal: to.terminal.clone(),
609 },
610 to: Endpoint {
611 at: to.at,
612 terminal: from.terminal.clone(),
613 },
614 stroke: stroke.clone(),
615 },
616 Self::Box { .. } | Self::Line { .. } => self.clone(),
617 }
618 }
619
620 #[must_use]
631 pub fn with_endpoint_moved(&self, end: End, by: Delta, out: i16) -> Self {
632 let Self::Connector { from, to, stroke } = self else {
633 return self.clone();
634 };
635
636 let move_endpoint = |ep: &Endpoint| -> Endpoint {
637 match ep.at {
638 EndpointAt::Absolute { at, leaving } => Endpoint {
639 at: EndpointAt::Absolute {
640 at: by.apply(at),
641 leaving,
642 },
643 terminal: ep.terminal.clone(),
644 },
645 EndpointAt::Reference(reference) => Endpoint {
646 at: EndpointAt::Reference(Reference {
647 id: reference.id,
648 anchor: reference.anchor,
649 offset: reference.offset.grow(by),
650 out: reference.out.saturating_add(out),
651 }),
652 terminal: ep.terminal.clone(),
653 },
654 }
655 };
656
657 match end {
658 End::From => Self::Connector {
659 from: move_endpoint(from),
660 to: to.clone(),
661 stroke: stroke.clone(),
662 },
663 End::To => Self::Connector {
664 from: from.clone(),
665 to: move_endpoint(to),
666 stroke: stroke.clone(),
667 },
668 }
669 }
670
671 #[must_use]
678 pub fn with_endpoint_reset(&self, end: End) -> Self {
679 let Self::Connector { from, to, stroke } = self else {
680 return self.clone();
681 };
682
683 let reset_endpoint = |ep: &Endpoint| match ep.at {
684 EndpointAt::Reference(reference) => Endpoint {
685 at: EndpointAt::Reference(Reference {
686 offset: Delta::ZERO,
687 out: 0,
688 ..reference
689 }),
690 terminal: ep.terminal.clone(),
691 },
692 EndpointAt::Absolute { .. } => ep.clone(),
693 };
694
695 match end {
696 End::From => Self::Connector {
697 from: reset_endpoint(from),
698 to: to.clone(),
699 stroke: stroke.clone(),
700 },
701 End::To => Self::Connector {
702 from: from.clone(),
703 to: reset_endpoint(to),
704 stroke: stroke.clone(),
705 },
706 }
707 }
708
709 #[must_use]
715 pub fn with_endpoint_terminal(&self, end: End, terminal: Terminal) -> Self {
716 let Self::Connector { from, to, stroke } = self else {
717 return self.clone();
718 };
719
720 match end {
721 End::From => Self::Connector {
722 from: Endpoint {
723 at: from.at,
724 terminal,
725 },
726 to: to.clone(),
727 stroke: stroke.clone(),
728 },
729 End::To => Self::Connector {
730 from: from.clone(),
731 to: Endpoint {
732 at: to.at,
733 terminal,
734 },
735 stroke: stroke.clone(),
736 },
737 }
738 }
739
740 #[must_use]
749 pub fn with_endpoint_detached(&self, end: End, diagram: &Diagram) -> Self {
750 let Self::Connector { from, to, stroke } = self else {
751 return self.clone();
752 };
753
754 let detach_endpoint = |ep: &Endpoint| match ep.at {
755 EndpointAt::Reference(reference) => {
756 if let Some(at) = ep.resolve(diagram) {
757 Endpoint {
758 at: EndpointAt::Absolute {
759 at,
760 leaving: reference.anchor.leaving(),
761 },
762 terminal: ep.terminal.clone(),
763 }
764 } else {
765 ep.clone()
766 }
767 }
768 EndpointAt::Absolute { .. } => ep.clone(),
769 };
770
771 match end {
772 End::From => Self::Connector {
773 from: detach_endpoint(from),
774 to: to.clone(),
775 stroke: stroke.clone(),
776 },
777 End::To => Self::Connector {
778 from: from.clone(),
779 to: detach_endpoint(to),
780 stroke: stroke.clone(),
781 },
782 }
783 }
784
785 #[must_use]
794 pub fn with_endpoint_attached(
795 &self,
796 end: End,
797 reference: Reference,
798 diagram: &Diagram,
799 ) -> Self {
800 let Self::Connector { from, to, stroke } = self else {
801 return self.clone();
802 };
803
804 if diagram
805 .get(reference.id)
806 .and_then(|shape| shape.anchor(reference.anchor))
807 .is_none()
808 {
809 return self.clone();
810 }
811
812 let attach_endpoint = |ep: &Endpoint| match ep.at {
813 EndpointAt::Absolute { .. } => Endpoint {
814 at: EndpointAt::Reference(reference),
815 terminal: ep.terminal.clone(),
816 },
817 EndpointAt::Reference(_) => ep.clone(),
818 };
819
820 match end {
821 End::From => Self::Connector {
822 from: attach_endpoint(from),
823 to: to.clone(),
824 stroke: stroke.clone(),
825 },
826 End::To => Self::Connector {
827 from: from.clone(),
828 to: attach_endpoint(to),
829 stroke: stroke.clone(),
830 },
831 }
832 }
833
834 #[must_use]
842 pub fn with_endpoint_leaving(&self, end: End, leaving: Direction) -> Self {
843 let Self::Connector { from, to, stroke } = self else {
844 return self.clone();
845 };
846
847 let update_endpoint = |ep: &Endpoint| match ep.at {
848 EndpointAt::Absolute { at, .. } => Endpoint {
849 at: EndpointAt::Absolute { at, leaving },
850 terminal: ep.terminal.clone(),
851 },
852 EndpointAt::Reference(_) => ep.clone(),
853 };
854
855 match end {
856 End::From => Self::Connector {
857 from: update_endpoint(from),
858 to: to.clone(),
859 stroke: stroke.clone(),
860 },
861 End::To => Self::Connector {
862 from: from.clone(),
863 to: update_endpoint(to),
864 stroke: stroke.clone(),
865 },
866 }
867 }
868
869 #[must_use]
898 pub(crate) fn with_frozen_references(&self, id: ShapeId, diagram: &Diagram) -> Option<Self> {
899 match self {
900 Self::Box { .. } | Self::Line { .. } => None,
901 Self::Connector { from, to, stroke } => match (
902 from.frozen_position(id, diagram),
903 to.frozen_position(id, diagram),
904 ) {
905 (None, None) => None,
906 (new_from, new_to) => Some(Self::Connector {
907 from: Endpoint {
908 at: new_from.unwrap_or(from.at),
909 ..from.clone()
910 },
911 to: Endpoint {
912 at: new_to.unwrap_or(to.at),
913 ..to.clone()
914 },
915 stroke: stroke.clone(),
916 }),
917 },
918 }
919 }
920}
921
922fn clamp_dimension(current: u16, delta: i16, floor: u16) -> u16 {
929 let min = i64::from(current.min(floor));
930 let max = i64::from(u16::MAX);
931 let target = i64::from(current) + i64::from(delta);
932 u16::try_from(target.clamp(min, max)).expect("clamped to u16 range")
933}
934
935#[cfg(test)]
936mod tests {
937 use std::num::NonZeroU16;
938
939 use super::{Delta, End, Endpoint, EndpointAt, Shape};
940 use crate::{Anchor, Diagram, Reference, ShapeId};
941 use monospace_core::{
942 Buffer, CellReader, Direction, FigureLayer, Glyph, GlyphCatalog, Orientation, Pos, Size,
943 StampMode, Stroke, Terminal, Text, Window, render,
944 };
945
946 const ORIGIN: Pos = Pos { x: 0, y: 0 };
949 const WINDOW: Size = Size {
950 width: 11,
951 height: 3,
952 };
953
954 fn light() -> Stroke {
955 Stroke::from("light")
956 }
957
958 fn box_at(x: i16) -> Shape {
959 Shape::Box {
960 at: Pos { x, y: 0 },
961 size: Size {
962 width: 3,
963 height: 3,
964 },
965 stroke: light(),
966 fill: None,
967 text: None,
968 }
969 }
970
971 fn connector(from: Terminal, to: Terminal) -> Shape {
974 Shape::Connector {
975 from: Endpoint::absolute(Pos { x: 2, y: 1 }, Direction::Right, from),
976 to: Endpoint::absolute(Pos { x: 8, y: 1 }, Direction::Left, to),
977 stroke: light(),
978 }
979 }
980
981 fn line_at(x: i16) -> Shape {
983 Shape::Line {
984 at: Pos { x, y: 0 },
985 len: 3,
986 orientation: Orientation::Horizontal,
987 stroke: light(),
988 }
989 }
990
991 fn hanging_from(box_id: ShapeId) -> Shape {
994 Shape::Connector {
995 from: Endpoint::reference(
996 Reference::new(box_id, Anchor::Right, Delta { dx: 0, dy: 0 }, 0),
997 Terminal::Arm,
998 ),
999 to: Endpoint::absolute(Pos { x: 7, y: 1 }, Direction::Left, Terminal::Arm),
1000 stroke: light(),
1001 }
1002 }
1003
1004 #[test]
1027 fn a_figure_holding_no_reference_answers_nothing_and_one_holding_one_freezes() {
1028 let beside = Diagram::new();
1029 let (beside, named) = beside.add(box_at(0));
1030 let (beside, another) = beside.add(box_at(8));
1031
1032 for holds_nothing in [
1035 box_at(0),
1036 line_at(0),
1037 connector(Terminal::Arm, Terminal::Arm),
1038 hanging_from(another),
1039 ] {
1040 assert_eq!(
1041 holds_nothing.with_frozen_references(named, &beside),
1042 None,
1043 "{holds_nothing:?} held nothing to freeze, so this is not that case"
1044 );
1045 }
1046
1047 let holds_a_reference = hanging_from(named);
1048 let freezes = holds_a_reference
1049 .with_frozen_references(named, &beside)
1050 .expect("a connector naming the figure it is asked about freezes");
1051 let Shape::Connector { from, .. } = &freezes else {
1052 unreachable!("the figure above is a connector")
1053 };
1054 assert_eq!(
1055 from.at,
1056 EndpointAt::Absolute {
1057 at: Pos { x: 2, y: 1 },
1058 leaving: Direction::Right,
1059 },
1060 "the end did not freeze at the point its reference was resolving to"
1061 );
1062 assert_ne!(
1063 freezes, holds_a_reference,
1064 "the frozen figure is the one that went in, so nothing changed"
1065 );
1066 }
1067
1068 #[test]
1076 fn a_figure_displaced_by_nothing_comes_back_equal_to_itself() {
1077 let nothing = Delta { dx: 0, dy: 0 };
1078
1079 for shape in [
1080 box_at(0),
1081 line_at(0),
1082 connector(Terminal::Arm, Terminal::Arm),
1083 ] {
1084 assert_eq!(shape.displaced_by(nothing), shape);
1085 }
1086 }
1087
1088 fn draw(shapes: Vec<Shape>) -> Buffer {
1089 let mut diagram = Diagram::new();
1090 for shape in shapes {
1091 diagram = diagram.add(shape).0;
1092 }
1093 let mut buffer = Buffer::new(Window {
1094 origin: ORIGIN,
1095 size: WINDOW,
1096 });
1097 diagram.draw(&mut buffer);
1098 buffer
1099 }
1100
1101 fn both_orders(from: Terminal, to: Terminal) -> (Buffer, Buffer) {
1106 (
1107 draw(vec![
1108 box_at(0),
1109 connector(from.clone(), to.clone()),
1110 box_at(8),
1111 ]),
1112 draw(vec![box_at(0), box_at(8), connector(from, to)]),
1113 )
1114 }
1115
1116 fn text_of(buffer: &Buffer) -> String {
1117 render(buffer, &GlyphCatalog::light(), buffer.window())
1118 }
1119
1120 fn glyph_terminals() -> (Terminal, Terminal) {
1121 (
1122 Terminal::Glyph {
1123 glyph: Glyph::new("◄").expect("one glyph"),
1124 },
1125 Terminal::Glyph {
1126 glyph: Glyph::new("►").expect("one glyph"),
1127 },
1128 )
1129 }
1130
1131 #[test]
1135 fn glyph_terminals_draw_two_different_pictures_and_the_border_cell_is_lost_in_one() {
1136 let (from, to) = glyph_terminals();
1137 let (second_box_after, second_box_before) = both_orders(from, to);
1138
1139 assert_ne!(text_of(&second_box_after), text_of(&second_box_before));
1140 assert_eq!(
1141 text_of(&second_box_after),
1142 concat!("┌─┐ ┌─┐\n", "│ ◄─────│ │\n", "└─┘ └─┘\n")
1143 );
1144 assert_eq!(
1145 text_of(&second_box_before),
1146 concat!("┌─┐ ┌─┐\n", "│ ◄─────► │\n", "└─┘ └─┘\n")
1147 );
1148
1149 let at = Pos { x: 2, y: 1 };
1155 for buffer in [&second_box_after, &second_box_before] {
1156 let cell = buffer.cell(at).expect("the connector writes this cell");
1157 assert!(cell.is_decided(), "{cell:?} at (2, 1) should be decided");
1158 }
1159 }
1160
1161 #[test]
1165 fn arm_terminals_draw_one_identical_picture_with_a_junction_in_both_orders() {
1166 let (second_box_after, second_box_before) = both_orders(Terminal::Arm, Terminal::Arm);
1167
1168 assert_eq!(text_of(&second_box_after), text_of(&second_box_before));
1169 assert_eq!(
1170 text_of(&second_box_after),
1171 concat!("┌─┐ ┌─┐\n", "│ ├─────┤ │\n", "└─┘ └─┘\n")
1172 );
1173
1174 for buffer in [&second_box_after, &second_box_before] {
1178 let cell = buffer
1179 .cell(Pos { x: 2, y: 1 })
1180 .expect("the connector writes this cell");
1181 assert!(!cell.is_decided(), "{cell:?} at (2, 1) should be undecided");
1182 }
1183 }
1184
1185 fn box_with_text(size: Size, content: &str) -> Shape {
1187 Shape::Box {
1188 at: ORIGIN,
1189 size,
1190 stroke: light(),
1191 fill: None,
1192 text: Some(
1193 Text::new(content)
1194 .unwrap_or_else(|| panic!("{content:?} has no control character")),
1195 ),
1196 }
1197 }
1198
1199 fn drawn_in(size: Size, shape: &Shape) -> String {
1200 let mut buffer = Buffer::new(Window {
1201 origin: ORIGIN,
1202 size,
1203 });
1204 shape.draw(
1205 &mut FigureLayer::new(&mut buffer, StampMode::Above),
1206 &Diagram::new(),
1207 );
1208 text_of(&buffer)
1209 }
1210
1211 #[test]
1213 fn a_box_centres_its_text_in_the_interior() {
1214 let size = Size {
1215 width: 8,
1216 height: 5,
1217 };
1218
1219 assert_eq!(
1220 drawn_in(size, &box_with_text(size, "hi")),
1221 "┌──────┐\n│ │\n│ hi │\n│ │\n└──────┘\n"
1222 );
1223 }
1224
1225 #[test]
1228 fn a_two_by_two_box_draws_its_text_on_the_corner() {
1229 let size = Size {
1230 width: 2,
1231 height: 2,
1232 };
1233
1234 assert_eq!(drawn_in(size, &box_with_text(size, "x")), "┌┐\n└x\n");
1235 }
1236
1237 #[test]
1240 fn a_box_centres_two_lines_in_the_interior() {
1241 let size = Size {
1242 width: 8,
1243 height: 5,
1244 };
1245
1246 assert_eq!(
1247 drawn_in(size, &box_with_text(size, "one\ntwo")),
1248 "┌──────┐\n│ one │\n│ two │\n│ │\n└──────┘\n"
1249 );
1250 }
1251
1252 #[test]
1255 fn a_box_draws_text_wider_than_its_interior() {
1256 let size = Size {
1257 width: 5,
1258 height: 3,
1259 };
1260
1261 assert_eq!(
1262 drawn_in(size, &box_with_text(size, "hello")),
1263 "┌───┐\nhello\n└───┘\n"
1264 );
1265 }
1266
1267 #[test]
1269 fn a_box_without_text_draws_what_it_drew_before() {
1270 let size = Size {
1271 width: 8,
1272 height: 5,
1273 };
1274 let without = Shape::Box {
1275 at: ORIGIN,
1276 size,
1277 stroke: light(),
1278 fill: None,
1279 text: None,
1280 };
1281
1282 assert_eq!(
1283 drawn_in(size, &without),
1284 "┌──────┐\n│ │\n│ │\n│ │\n└──────┘\n"
1285 );
1286 }
1287
1288 #[test]
1290 fn a_filled_box_draws_its_text_over_the_fill() {
1291 let size = Size {
1292 width: 6,
1293 height: 3,
1294 };
1295 let filled = Shape::Box {
1296 at: ORIGIN,
1297 size,
1298 stroke: light(),
1299 fill: Some(Glyph::new("░").expect("\"░\" is one glyph")),
1300 text: Text::new("hi"),
1301 };
1302
1303 assert_eq!(drawn_in(size, &filled), "┌────┐\n│░hi░│\n└────┘\n");
1304 }
1305
1306 #[test]
1309 fn displacing_a_box_moves_its_text() {
1310 let size = Size {
1311 width: 8,
1312 height: 5,
1313 };
1314 let displaced = box_with_text(size, "hi").displaced_by(Delta { dx: 2, dy: 1 });
1315
1316 assert_eq!(
1317 drawn_in(
1318 Size {
1319 width: 10,
1320 height: 6,
1321 },
1322 &displaced,
1323 ),
1324 concat!(
1325 " \n",
1326 " ┌──────┐\n",
1327 " │ │\n",
1328 " │ hi │\n",
1329 " │ │\n",
1330 " └──────┘\n",
1331 )
1332 );
1333 }
1334
1335 #[test]
1338 fn a_box_with_text_answers_the_same_anchors() {
1339 let size = Size {
1340 width: 8,
1341 height: 5,
1342 };
1343 let without = box_with_text(size, "");
1344 let with = box_with_text(size, "hi");
1345
1346 for anchor in [Anchor::Top, Anchor::Right, Anchor::Bottom, Anchor::Left] {
1347 assert_eq!(with.anchor(anchor), without.anchor(anchor), "{anchor:?}");
1348 }
1349 }
1350
1351 #[test]
1355 fn an_attached_endpoint_derives_its_leaving_direction_and_preserves_it_on_freeze() {
1356 use monospace_core::SymbolStyleName;
1357
1358 let style = SymbolStyleName::from("solid");
1359 let nothing = Delta { dx: 0, dy: 0 };
1360 let the_box = Shape::Box {
1361 at: Pos { x: 5, y: 5 },
1362 size: Size {
1363 width: 5,
1364 height: 5,
1365 },
1366 stroke: light(),
1367 fill: None,
1368 text: None,
1369 };
1370
1371 for (anchor, expected_leaving, far_point, far_leaving) in [
1372 (
1373 Anchor::Top,
1374 Direction::Up,
1375 Pos { x: 7, y: 1 },
1376 Direction::Down,
1377 ),
1378 (
1379 Anchor::Right,
1380 Direction::Right,
1381 Pos { x: 13, y: 7 },
1382 Direction::Left,
1383 ),
1384 (
1385 Anchor::Bottom,
1386 Direction::Down,
1387 Pos { x: 7, y: 13 },
1388 Direction::Up,
1389 ),
1390 (
1391 Anchor::Left,
1392 Direction::Left,
1393 Pos { x: 1, y: 7 },
1394 Direction::Right,
1395 ),
1396 ] {
1397 let diagram = Diagram::new();
1398 let (d, box_id) = diagram.add(the_box.clone());
1399 let from_endpoint = Endpoint::reference(
1400 Reference::new(box_id, anchor, nothing, 0),
1401 Terminal::Symbol {
1402 style: style.clone(),
1403 },
1404 );
1405 assert_eq!(from_endpoint.leaving(), expected_leaving);
1406 assert_eq!(from_endpoint.at.leaving(), expected_leaving);
1407
1408 let (d, connector_id) = d.add(Shape::Connector {
1409 from: from_endpoint,
1410 to: Endpoint::absolute(far_point, far_leaving, Terminal::Arm),
1411 stroke: light(),
1412 });
1413
1414 let mut buffer = Buffer::new(Window {
1416 origin: Pos { x: 0, y: 0 },
1417 size: Size {
1418 width: 15,
1419 height: 15,
1420 },
1421 });
1422 d.draw(&mut buffer);
1423
1424 let d_after = d.remove(box_id);
1426 let Shape::Connector {
1427 from: frozen_from, ..
1428 } = d_after
1429 .get(connector_id)
1430 .expect("connector still in diagram")
1431 else {
1432 unreachable!()
1433 };
1434
1435 let border_at = the_box.anchor(anchor).expect("box answers all anchors");
1436 let expected_at = anchor.outward(1).apply(border_at);
1437 assert_eq!(
1438 frozen_from.at,
1439 EndpointAt::Absolute {
1440 at: expected_at,
1441 leaving: expected_leaving,
1442 }
1443 );
1444 assert_eq!(frozen_from.leaving(), expected_leaving);
1445 }
1446 }
1447
1448 #[test]
1449 fn a_resized_box_keeps_its_position_stroke_fill_and_text() {
1450 let size = Size {
1451 width: 8,
1452 height: 5,
1453 };
1454 let original = box_with_text(size, "hi");
1455 let resized = original.resized_by(Delta { dx: 2, dy: 1 });
1456
1457 let Shape::Box {
1458 at,
1459 size: new_size,
1460 stroke,
1461 fill,
1462 text,
1463 } = resized
1464 else {
1465 panic!("expected box");
1466 };
1467
1468 assert_eq!(at, Pos { x: 0, y: 0 });
1469 assert_eq!(
1470 new_size,
1471 Size {
1472 width: 10,
1473 height: 6
1474 }
1475 );
1476 assert_eq!(stroke, Stroke::from("light"));
1477 assert_eq!(fill, None);
1478 assert_eq!(text, Some(Text::new("hi").unwrap()));
1479 }
1480
1481 #[test]
1482 fn a_box_does_not_shrink_below_two() {
1483 let the_box = Shape::Box {
1484 at: Pos { x: 0, y: 0 },
1485 size: Size {
1486 width: 2,
1487 height: 2,
1488 },
1489 stroke: light(),
1490 fill: None,
1491 text: None,
1492 };
1493
1494 let shrunk = the_box.resized_by(Delta { dx: -1, dy: -1 });
1495 let Shape::Box { size, .. } = shrunk else {
1496 panic!("expected box");
1497 };
1498 assert_eq!(
1499 size,
1500 Size {
1501 width: 2,
1502 height: 2
1503 }
1504 );
1505 }
1506
1507 #[test]
1508 fn a_box_below_two_is_not_shrunk_further() {
1509 let the_box = Shape::Box {
1510 at: Pos { x: 0, y: 0 },
1511 size: Size {
1512 width: 1,
1513 height: 5,
1514 },
1515 stroke: light(),
1516 fill: None,
1517 text: None,
1518 };
1519
1520 let shrunk = the_box.resized_by(Delta { dx: -1, dy: 0 });
1521 let Shape::Box { size, .. } = shrunk else {
1522 panic!("expected box");
1523 };
1524 assert_eq!(size.width, 1);
1525
1526 let grown = the_box.resized_by(Delta { dx: 1, dy: 0 });
1527 let Shape::Box {
1528 size: grown_size, ..
1529 } = grown
1530 else {
1531 panic!("expected box");
1532 };
1533 assert_eq!(grown_size.width, 2);
1534 }
1535
1536 #[test]
1537 fn growth_stops_at_the_largest_size() {
1538 let the_box = Shape::Box {
1539 at: Pos { x: 0, y: 0 },
1540 size: Size {
1541 width: u16::MAX,
1542 height: u16::MAX,
1543 },
1544 stroke: light(),
1545 fill: None,
1546 text: None,
1547 };
1548
1549 let grown = the_box.resized_by(Delta { dx: 1, dy: 1 });
1550 let Shape::Box { size, .. } = grown else {
1551 panic!("expected box");
1552 };
1553 assert_eq!(
1554 size,
1555 Size {
1556 width: u16::MAX,
1557 height: u16::MAX
1558 }
1559 );
1560 }
1561
1562 #[test]
1563 fn a_horizontal_line_is_resized_along_its_row() {
1564 let line = Shape::Line {
1565 at: Pos { x: 2, y: 3 },
1566 len: 4,
1567 orientation: Orientation::Horizontal,
1568 stroke: light(),
1569 };
1570 assert_eq!(
1571 line.resized_by(Delta { dx: 1, dy: 0 }),
1572 Shape::Line {
1573 at: Pos { x: 2, y: 3 },
1574 len: 5,
1575 orientation: Orientation::Horizontal,
1576 stroke: light(),
1577 }
1578 );
1579 assert_eq!(
1580 line.resized_by(Delta { dx: -1, dy: 0 }),
1581 Shape::Line {
1582 at: Pos { x: 2, y: 3 },
1583 len: 3,
1584 orientation: Orientation::Horizontal,
1585 stroke: light(),
1586 }
1587 );
1588 assert_eq!(line.resized_by(Delta { dx: 0, dy: 1 }), line);
1589 assert_eq!(line.resized_by(Delta { dx: 0, dy: -1 }), line);
1590 }
1591
1592 #[test]
1593 fn a_vertical_line_is_resized_along_its_column() {
1594 let line = Shape::Line {
1595 at: Pos { x: 2, y: 3 },
1596 len: 4,
1597 orientation: Orientation::Vertical,
1598 stroke: light(),
1599 };
1600 assert_eq!(
1601 line.resized_by(Delta { dx: 0, dy: 1 }),
1602 Shape::Line {
1603 at: Pos { x: 2, y: 3 },
1604 len: 5,
1605 orientation: Orientation::Vertical,
1606 stroke: light(),
1607 }
1608 );
1609 assert_eq!(
1610 line.resized_by(Delta { dx: 0, dy: -1 }),
1611 Shape::Line {
1612 at: Pos { x: 2, y: 3 },
1613 len: 3,
1614 orientation: Orientation::Vertical,
1615 stroke: light(),
1616 }
1617 );
1618 assert_eq!(line.resized_by(Delta { dx: 1, dy: 0 }), line);
1619 assert_eq!(line.resized_by(Delta { dx: -1, dy: 0 }), line);
1620 }
1621
1622 #[test]
1623 fn a_line_keeps_its_position_orientation_and_stroke_when_resized() {
1624 let stroke = Stroke::from("heavy");
1625 let line = Shape::Line {
1626 at: Pos { x: 5, y: 7 },
1627 len: 4,
1628 orientation: Orientation::Horizontal,
1629 stroke: stroke.clone(),
1630 };
1631 let resized = line.resized_by(Delta { dx: 2, dy: 0 });
1632 let Shape::Line {
1633 at,
1634 len,
1635 orientation,
1636 stroke: actual_stroke,
1637 } = resized
1638 else {
1639 panic!("expected line");
1640 };
1641 assert_eq!(at, Pos { x: 5, y: 7 });
1642 assert_eq!(len, 6);
1643 assert_eq!(orientation, Orientation::Horizontal);
1644 assert_eq!(actual_stroke, stroke);
1645 }
1646
1647 #[test]
1648 fn a_line_does_not_shrink_below_one() {
1649 let line = Shape::Line {
1650 at: Pos { x: 0, y: 0 },
1651 len: 5,
1652 orientation: Orientation::Horizontal,
1653 stroke: light(),
1654 };
1655 assert_eq!(
1656 line.resized_by(Delta { dx: -1, dy: 0 }),
1657 Shape::Line {
1658 at: Pos { x: 0, y: 0 },
1659 len: 4,
1660 orientation: Orientation::Horizontal,
1661 stroke: light(),
1662 }
1663 );
1664 let min_line = Shape::Line {
1665 at: Pos { x: 0, y: 0 },
1666 len: 1,
1667 orientation: Orientation::Horizontal,
1668 stroke: light(),
1669 };
1670 assert_eq!(min_line.resized_by(Delta { dx: -1, dy: 0 }), min_line);
1671 assert_eq!(min_line.resized_by(Delta { dx: -50, dy: 0 }), min_line);
1672 }
1673
1674 #[test]
1675 fn a_zero_length_line_is_not_shrunk_and_grows_to_one() {
1676 let h_zero = Shape::Line {
1677 at: Pos { x: 0, y: 0 },
1678 len: 0,
1679 orientation: Orientation::Horizontal,
1680 stroke: light(),
1681 };
1682 assert_eq!(h_zero.resized_by(Delta { dx: -1, dy: 0 }), h_zero);
1683 assert_eq!(
1684 h_zero.resized_by(Delta { dx: 1, dy: 0 }),
1685 Shape::Line {
1686 at: Pos { x: 0, y: 0 },
1687 len: 1,
1688 orientation: Orientation::Horizontal,
1689 stroke: light(),
1690 }
1691 );
1692
1693 let v_zero = Shape::Line {
1694 at: Pos { x: 0, y: 0 },
1695 len: 0,
1696 orientation: Orientation::Vertical,
1697 stroke: light(),
1698 };
1699 assert_eq!(v_zero.resized_by(Delta { dx: 0, dy: -1 }), v_zero);
1700 assert_eq!(
1701 v_zero.resized_by(Delta { dx: 0, dy: 1 }),
1702 Shape::Line {
1703 at: Pos { x: 0, y: 0 },
1704 len: 1,
1705 orientation: Orientation::Vertical,
1706 stroke: light(),
1707 }
1708 );
1709 }
1710
1711 #[test]
1712 fn a_line_grows_no_further_than_u16_max() {
1713 let max_line = Shape::Line {
1714 at: Pos { x: 0, y: 0 },
1715 len: u16::MAX,
1716 orientation: Orientation::Horizontal,
1717 stroke: light(),
1718 };
1719 assert_eq!(max_line.resized_by(Delta { dx: 1, dy: 0 }), max_line);
1720 }
1721
1722 #[test]
1723 fn a_connector_is_not_resized() {
1724 let conn = connector(Terminal::Arm, Terminal::Arm);
1725 assert_eq!(conn.resized_by(Delta { dx: 2, dy: 3 }), conn);
1726 }
1727
1728 #[test]
1729 fn a_box_and_a_line_can_be_resized_and_a_connector_cannot() {
1730 assert!(box_at(0).can_resize());
1731 assert!(line_at(0).can_resize());
1732 assert!(!connector(Terminal::Arm, Terminal::Arm).can_resize());
1733 }
1734
1735 #[test]
1736 fn a_connector_is_never_resized() {
1737 let delta = Delta { dx: 2, dy: 3 };
1738 let conn = connector(Terminal::Arm, Terminal::Arm);
1739 assert!(!conn.can_resize());
1740 assert_eq!(conn.resized_by(delta), conn);
1741 }
1742
1743 #[test]
1744 fn a_box_can_be_resized() {
1745 let the_box = box_at(0);
1746 assert!(the_box.can_resize());
1747 assert_ne!(the_box.resized_by(Delta { dx: 2, dy: 3 }), the_box);
1748 }
1749
1750 #[test]
1751 fn a_box_changes_only_its_stroke() {
1752 let the_box = Shape::Box {
1753 at: Pos { x: 2, y: 3 },
1754 size: Size {
1755 width: 5,
1756 height: 4,
1757 },
1758 stroke: light(),
1759 fill: Some(Glyph::new("x").unwrap()),
1760 text: Some(Text::new("hi").unwrap()),
1761 };
1762 let double = Stroke::from("double");
1763 let updated = the_box.with_stroke(double.clone());
1764 assert_eq!(
1765 updated,
1766 Shape::Box {
1767 at: Pos { x: 2, y: 3 },
1768 size: Size {
1769 width: 5,
1770 height: 4,
1771 },
1772 stroke: double,
1773 fill: Some(Glyph::new("x").unwrap()),
1774 text: Some(Text::new("hi").unwrap()),
1775 }
1776 );
1777 }
1778
1779 #[test]
1780 fn a_line_changes_only_its_stroke() {
1781 let the_line = Shape::Line {
1782 at: Pos { x: 1, y: 2 },
1783 len: 4,
1784 orientation: Orientation::Vertical,
1785 stroke: light(),
1786 };
1787 let heavy = Stroke::from("heavy");
1788 let updated = the_line.with_stroke(heavy.clone());
1789 assert_eq!(
1790 updated,
1791 Shape::Line {
1792 at: Pos { x: 1, y: 2 },
1793 len: 4,
1794 orientation: Orientation::Vertical,
1795 stroke: heavy,
1796 }
1797 );
1798 }
1799
1800 #[test]
1801 fn a_connector_changes_only_its_stroke() {
1802 let the_connector = connector(Terminal::Arm, Terminal::Arm);
1803 let ascii = Stroke::from("ascii");
1804 let updated = the_connector.with_stroke(ascii.clone());
1805 match (the_connector, updated) {
1806 (
1807 Shape::Connector {
1808 from: f1,
1809 to: t1,
1810 stroke: s1,
1811 },
1812 Shape::Connector {
1813 from: f2,
1814 to: t2,
1815 stroke: s2,
1816 },
1817 ) => {
1818 assert_eq!(f1, f2);
1819 assert_eq!(t1, t2);
1820 assert_eq!(s1, light());
1821 assert_eq!(s2, ascii);
1822 }
1823 _ => unreachable!(),
1824 }
1825 }
1826
1827 #[test]
1828 fn with_rotated_orientation_toggles_a_line_and_keeps_position_length_and_stroke() {
1829 let horizontal = Shape::Line {
1830 at: Pos { x: 2, y: 3 },
1831 len: 5,
1832 orientation: Orientation::Horizontal,
1833 stroke: light(),
1834 };
1835 let rotated = horizontal.with_rotated_orientation();
1836 assert_eq!(
1837 rotated,
1838 Shape::Line {
1839 at: Pos { x: 2, y: 3 },
1840 len: 5,
1841 orientation: Orientation::Vertical,
1842 stroke: light(),
1843 }
1844 );
1845 assert_eq!(rotated.with_rotated_orientation(), horizontal);
1846 }
1847
1848 #[test]
1849 fn with_rotated_orientation_on_a_box_or_connector_changes_nothing() {
1850 let the_box = Shape::Box {
1851 at: Pos { x: 2, y: 3 },
1852 size: Size {
1853 width: 5,
1854 height: 4,
1855 },
1856 stroke: light(),
1857 fill: None,
1858 text: None,
1859 };
1860 assert_eq!(the_box.with_rotated_orientation(), the_box);
1861
1862 let the_connector = connector(Terminal::Arm, Terminal::Arm);
1863 assert_eq!(the_connector.with_rotated_orientation(), the_connector);
1864 }
1865
1866 #[test]
1867 fn with_text_changes_only_a_box() {
1868 let the_box = Shape::Box {
1869 at: Pos { x: 2, y: 3 },
1870 size: Size {
1871 width: 5,
1872 height: 4,
1873 },
1874 stroke: light(),
1875 fill: Some(Glyph::new("x").unwrap()),
1876 text: None,
1877 };
1878 let text = Text::new("hi").unwrap();
1879
1880 assert_eq!(
1881 the_box.with_text(Some(text.clone())),
1882 Shape::Box {
1883 at: Pos { x: 2, y: 3 },
1884 size: Size {
1885 width: 5,
1886 height: 4,
1887 },
1888 stroke: light(),
1889 fill: Some(Glyph::new("x").unwrap()),
1890 text: Some(text),
1891 }
1892 );
1893 }
1894
1895 #[test]
1896 fn with_text_on_a_line_or_connector_changes_nothing() {
1897 let the_line = Shape::Line {
1898 at: Pos { x: 1, y: 2 },
1899 len: 4,
1900 orientation: Orientation::Vertical,
1901 stroke: light(),
1902 };
1903 let the_connector = connector(Terminal::Arm, Terminal::Arm);
1904 let text = Text::new("hi").unwrap();
1905
1906 assert_eq!(the_line.with_text(Some(text.clone())), the_line);
1907 assert_eq!(the_connector.with_text(Some(text)), the_connector);
1908 }
1909
1910 #[test]
1911 fn setting_the_same_text_is_no_change() {
1912 let text = Text::new("hi").unwrap();
1913 let the_box = Shape::Box {
1914 at: Pos { x: 2, y: 3 },
1915 size: Size {
1916 width: 5,
1917 height: 4,
1918 },
1919 stroke: light(),
1920 fill: None,
1921 text: Some(text.clone()),
1922 };
1923
1924 assert_eq!(the_box.with_text(Some(text)), the_box);
1925 }
1926
1927 #[test]
1928 fn with_inverted_terminals_swaps_from_and_to_terminals_on_a_connector() {
1929 use monospace_core::SymbolStyleName;
1930
1931 let arm = Terminal::Arm;
1932 let solid = Terminal::Symbol {
1933 style: SymbolStyleName::from("solid"),
1934 };
1935 let connector = connector(arm.clone(), solid.clone());
1936 let inverted = connector.with_inverted_terminals();
1937
1938 match (connector, inverted) {
1939 (
1940 Shape::Connector {
1941 from: f1,
1942 to: t1,
1943 stroke: s1,
1944 },
1945 Shape::Connector {
1946 from: f2,
1947 to: t2,
1948 stroke: s2,
1949 },
1950 ) => {
1951 assert_eq!(f1.terminal, arm);
1952 assert_eq!(t1.terminal, solid);
1953 assert_eq!(f2.terminal, solid);
1954 assert_eq!(t2.terminal, arm);
1955 assert_eq!(f1.at, f2.at);
1956 assert_eq!(t1.at, t2.at);
1957 assert_eq!(s1, s2);
1958 }
1959 _ => unreachable!(),
1960 }
1961 }
1962
1963 #[test]
1964 fn with_inverted_terminals_on_a_box_or_line_changes_nothing() {
1965 let the_box = Shape::Box {
1966 at: Pos { x: 2, y: 3 },
1967 size: Size {
1968 width: 5,
1969 height: 4,
1970 },
1971 stroke: light(),
1972 fill: None,
1973 text: None,
1974 };
1975 let the_line = Shape::Line {
1976 at: Pos { x: 1, y: 2 },
1977 len: 4,
1978 orientation: Orientation::Vertical,
1979 stroke: light(),
1980 };
1981
1982 assert_eq!(the_box.with_inverted_terminals(), the_box);
1983 assert_eq!(the_line.with_inverted_terminals(), the_line);
1984 }
1985
1986 #[test]
1987 fn with_inverted_terminals_preserves_endpoint_geometry_and_attachments() {
1988 use monospace_core::SymbolStyleName;
1989
1990 let (_, box_id) = Diagram::new().add(box_at(0));
1991 let from_at = EndpointAt::Reference(Reference::new(
1992 box_id,
1993 Anchor::Right,
1994 Delta { dx: 1, dy: 2 },
1995 3,
1996 ));
1997 let to_at = EndpointAt::Absolute {
1998 at: Pos { x: 15, y: 8 },
1999 leaving: Direction::Left,
2000 };
2001
2002 let conn = Shape::Connector {
2003 from: Endpoint {
2004 at: from_at,
2005 terminal: Terminal::Arm,
2006 },
2007 to: Endpoint {
2008 at: to_at,
2009 terminal: Terminal::Symbol {
2010 style: SymbolStyleName::from("solid"),
2011 },
2012 },
2013 stroke: light(),
2014 };
2015
2016 let inverted = conn.with_inverted_terminals();
2017 match inverted {
2018 Shape::Connector { from, to, stroke } => {
2019 assert_eq!(from.at, from_at);
2020 assert_eq!(to.at, to_at);
2021 assert_eq!(
2022 from.terminal,
2023 Terminal::Symbol {
2024 style: SymbolStyleName::from("solid")
2025 }
2026 );
2027 assert_eq!(to.terminal, Terminal::Arm);
2028 assert_eq!(stroke, light());
2029 }
2030 _ => unreachable!(),
2031 }
2032 }
2033
2034 #[test]
2035 fn effective_out_offsets_non_arm_terminals_by_one_and_is_additive() {
2036 use monospace_core::SymbolStyleName;
2037
2038 let nothing = Delta { dx: 0, dy: 0 };
2039 let symbol = Terminal::Symbol {
2040 style: SymbolStyleName::from("solid"),
2041 };
2042 let glyph = Terminal::Glyph {
2043 glyph: Glyph::new("x").unwrap(),
2044 };
2045 let arm = Terminal::Arm;
2046
2047 let (_, dummy_id) = Diagram::new().add(box_at(0));
2048 let at_ref =
2049 |out: i16| EndpointAt::Reference(Reference::new(dummy_id, Anchor::Right, nothing, out));
2050
2051 assert_eq!(at_ref(0).effective_out(&arm), 0);
2053 assert_eq!(
2054 Endpoint::reference(
2055 Reference::new(dummy_id, Anchor::Right, nothing, 0),
2056 arm.clone(),
2057 )
2058 .effective_out(),
2059 0
2060 );
2061
2062 assert_eq!(at_ref(0).effective_out(&symbol), 1);
2064 assert_eq!(at_ref(0).effective_out(&glyph), 1);
2065
2066 assert_eq!(at_ref(1).effective_out(&symbol), 2);
2068 assert_eq!(at_ref(2).effective_out(&symbol), 3);
2069
2070 assert_eq!(at_ref(-1).effective_out(&symbol), 0);
2072
2073 let absolute = EndpointAt::Absolute {
2075 at: Pos { x: 3, y: 3 },
2076 leaving: Direction::Right,
2077 };
2078 assert_eq!(absolute.effective_out(&symbol), 0);
2079 assert_eq!(absolute.effective_out(&arm), 0);
2080 }
2081
2082 #[test]
2083 fn endpoint_resolves_with_terminal_offsetting_non_arm_terminals_by_one() {
2084 use monospace_core::SymbolStyleName;
2085
2086 let nothing = Delta { dx: 0, dy: 0 };
2087 let symbol = Terminal::Symbol {
2088 style: SymbolStyleName::from("solid"),
2089 };
2090 let glyph = Terminal::Glyph {
2091 glyph: Glyph::new("x").unwrap(),
2092 };
2093 let arm = Terminal::Arm;
2094
2095 let the_box = Shape::Box {
2096 at: Pos { x: 5, y: 5 },
2097 size: Size {
2098 width: 5,
2099 height: 5,
2100 },
2101 stroke: light(),
2102 fill: None,
2103 text: None,
2104 };
2105
2106 let diagram = Diagram::new();
2107 let (diagram, box_id) = diagram.add(the_box);
2108
2109 for (anchor, expected_normal) in [
2111 (Anchor::Top, Delta { dx: 0, dy: -1 }),
2112 (Anchor::Right, Delta { dx: 1, dy: 0 }),
2113 (Anchor::Bottom, Delta { dx: 0, dy: 1 }),
2114 (Anchor::Left, Delta { dx: -1, dy: 0 }),
2115 ] {
2116 let border_point = diagram.get(box_id).unwrap().anchor(anchor).unwrap();
2117 let outward_point = expected_normal.apply(border_point);
2118
2119 let arm_ep =
2121 Endpoint::reference(Reference::new(box_id, anchor, nothing, 0), arm.clone());
2122 assert_eq!(arm_ep.resolve(&diagram), Some(border_point));
2123
2124 let symbol_ep =
2126 Endpoint::reference(Reference::new(box_id, anchor, nothing, 0), symbol.clone());
2127 assert_eq!(symbol_ep.resolve(&diagram), Some(outward_point));
2128
2129 let glyph_ep =
2131 Endpoint::reference(Reference::new(box_id, anchor, nothing, 0), glyph.clone());
2132 assert_eq!(glyph_ep.resolve(&diagram), Some(outward_point));
2133
2134 let outward_2 = Delta {
2136 dx: expected_normal.dx * 2,
2137 dy: expected_normal.dy * 2,
2138 }
2139 .apply(border_point);
2140 let out_1_ep =
2141 Endpoint::reference(Reference::new(box_id, anchor, nothing, 1), symbol.clone());
2142 assert_eq!(out_1_ep.resolve(&diagram), Some(outward_2));
2143
2144 let out_neg1_ep =
2146 Endpoint::reference(Reference::new(box_id, anchor, nothing, -1), symbol.clone());
2147 assert_eq!(out_neg1_ep.resolve(&diagram), Some(border_point));
2148
2149 let connector_from =
2151 Endpoint::reference(Reference::new(box_id, anchor, nothing, 0), symbol.clone());
2152 let connector_to =
2153 Endpoint::reference(Reference::new(box_id, anchor, nothing, 0), symbol.clone());
2154 assert_eq!(
2155 connector_from.resolve(&diagram),
2156 connector_to.resolve(&diagram)
2157 );
2158 }
2159
2160 let absent_id = ShapeId::new(std::num::NonZeroU16::new(999).unwrap());
2162 let absent_ep =
2163 Endpoint::reference(Reference::new(absent_id, Anchor::Top, nothing, 0), symbol);
2164 assert_eq!(absent_ep.resolve(&diagram), None);
2165 }
2166
2167 #[test]
2168 fn attached_connectors_draw_non_arm_terminals_one_cell_outward_preserving_borders() {
2169 use monospace_core::SymbolStyleName;
2170
2171 let nothing = Delta { dx: 0, dy: 0 };
2172 let symbol = Terminal::Symbol {
2173 style: SymbolStyleName::from("solid"),
2174 };
2175 let catalog = GlyphCatalog::union([GlyphCatalog::light(), GlyphCatalog::solid()]);
2176 let window = Window {
2177 origin: ORIGIN,
2178 size: WINDOW,
2179 };
2180
2181 let d = Diagram::new();
2183 let (d, left_id) = d.add(box_at(0));
2184 let (d, right_id) = d.add(box_at(8));
2185 let (d1, _) = d.add(Shape::Connector {
2186 from: Endpoint::reference(
2187 Reference::new(left_id, Anchor::Right, nothing, 0),
2188 Terminal::Arm,
2189 ),
2190 to: Endpoint::reference(
2191 Reference::new(right_id, Anchor::Left, nothing, 0),
2192 symbol.clone(),
2193 ),
2194 stroke: light(),
2195 });
2196
2197 let mut buf1 = Buffer::new(window);
2198 d1.draw(&mut buf1);
2199 assert_eq!(
2200 render(&buf1, &catalog, window),
2201 concat!("┌─┐ ┌─┐\n", "│ ├────▶│ │\n", "└─┘ └─┘\n")
2202 );
2203
2204 let d = Diagram::new();
2206 let (d, left_id) = d.add(box_at(0));
2207 let (d, right_id) = d.add(box_at(8));
2208 let (d3, _) = d.add(Shape::Connector {
2209 from: Endpoint::reference(
2210 Reference::new(left_id, Anchor::Right, nothing, 0),
2211 symbol.clone(),
2212 ),
2213 to: Endpoint::reference(Reference::new(right_id, Anchor::Left, nothing, 0), symbol),
2214 stroke: light(),
2215 });
2216
2217 let mut buf3 = Buffer::new(window);
2218 d3.draw(&mut buf3);
2219 assert_eq!(
2220 render(&buf3, &catalog, window),
2221 concat!("┌─┐ ┌─┐\n", "│ │◀───▶│ │\n", "└─┘ └─┘\n")
2222 );
2223
2224 let d = Diagram::new();
2226 let (d, left_id) = d.add(box_at(0));
2227 let (d, right_id) = d.add(box_at(8));
2228 let (da, _) = d.add(Shape::Connector {
2229 from: Endpoint::reference(
2230 Reference::new(left_id, Anchor::Right, nothing, 0),
2231 Terminal::Arm,
2232 ),
2233 to: Endpoint::reference(
2234 Reference::new(right_id, Anchor::Left, nothing, 0),
2235 Terminal::Arm,
2236 ),
2237 stroke: light(),
2238 });
2239
2240 let mut buf_junctions = Buffer::new(window);
2241 da.draw(&mut buf_junctions);
2242 assert_eq!(
2243 render(&buf_junctions, &catalog, window),
2244 concat!("┌─┐ ┌─┐\n", "│ ├─────┤ │\n", "└─┘ └─┘\n")
2245 );
2246 }
2247
2248 #[test]
2249 fn with_endpoint_moved_adjusts_attached_offset_on_both_axes_without_clamping() {
2250 let connector = Shape::Connector {
2251 from: Endpoint::reference(
2252 Reference::new(
2253 ShapeId::new(NonZeroU16::new(1).unwrap()),
2254 Anchor::Top,
2255 Delta::ZERO,
2256 0,
2257 ),
2258 Terminal::Arm,
2259 ),
2260 to: Endpoint::absolute(Pos { x: 20, y: 0 }, Direction::Down, Terminal::Arm),
2261 stroke: light(),
2262 };
2263
2264 let moved = connector.with_endpoint_moved(End::From, Delta { dx: 15, dy: -8 }, 0);
2266 let Shape::Connector { ref from, .. } = moved else {
2267 panic!()
2268 };
2269 let EndpointAt::Reference(ref r) = from.at else {
2270 panic!()
2271 };
2272 assert_eq!(r.offset, Delta { dx: 15, dy: -8 });
2273
2274 let moved_more = moved.with_endpoint_moved(End::From, Delta { dx: -5, dy: 10 }, 0);
2276 let Shape::Connector { from, .. } = moved_more else {
2277 panic!()
2278 };
2279 let EndpointAt::Reference(ref r) = from.at else {
2280 panic!()
2281 };
2282 assert_eq!(r.offset, Delta { dx: 10, dy: 2 });
2283 }
2284
2285 #[test]
2286 fn with_endpoint_moved_displaces_detached_endpoint_on_both_axes() {
2287 let connector = Shape::Connector {
2288 from: Endpoint::absolute(Pos { x: 5, y: 10 }, Direction::Right, Terminal::Arm),
2289 to: Endpoint::absolute(Pos { x: 20, y: 30 }, Direction::Left, Terminal::Arm),
2290 stroke: light(),
2291 };
2292
2293 let moved = connector.with_endpoint_moved(End::From, Delta { dx: 2, dy: -3 }, 5);
2294 let Shape::Connector { from, to, stroke } = moved else {
2295 panic!()
2296 };
2297 assert_eq!(
2298 from,
2299 Endpoint::absolute(Pos { x: 7, y: 7 }, Direction::Right, Terminal::Arm)
2300 );
2301 assert_eq!(
2302 to,
2303 Endpoint::absolute(Pos { x: 20, y: 30 }, Direction::Left, Terminal::Arm)
2304 );
2305 assert_eq!(stroke, light());
2306 }
2307
2308 #[test]
2309 fn with_endpoint_moved_adjusts_out_including_negative_values() {
2310 let connector = Shape::Connector {
2311 from: Endpoint::reference(
2312 Reference::new(
2313 ShapeId::new(NonZeroU16::new(1).unwrap()),
2314 Anchor::Top,
2315 Delta::ZERO,
2316 0,
2317 ),
2318 Terminal::Arm,
2319 ),
2320 to: Endpoint::absolute(Pos { x: 20, y: 0 }, Direction::Down, Terminal::Arm),
2321 stroke: light(),
2322 };
2323
2324 let moved_plus = connector.with_endpoint_moved(End::From, Delta::ZERO, 2);
2326 let Shape::Connector { from, .. } = moved_plus else {
2327 panic!()
2328 };
2329 let EndpointAt::Reference(ref r) = from.at else {
2330 panic!()
2331 };
2332 assert_eq!(r.out, 2);
2333
2334 let moved_neg = connector.with_endpoint_moved(End::From, Delta::ZERO, -3);
2336 let Shape::Connector { from, .. } = moved_neg else {
2337 panic!()
2338 };
2339 let EndpointAt::Reference(ref r) = from.at else {
2340 panic!()
2341 };
2342 assert_eq!(r.out, -3);
2343 }
2344
2345 #[test]
2346 fn with_endpoint_reset_clears_offset_and_out_on_targeted_reference_only() {
2347 let box_id = ShapeId::new(NonZeroU16::new(1).unwrap());
2348 let connector = Shape::Connector {
2349 from: Endpoint::reference(
2350 Reference::new(box_id, Anchor::Top, Delta { dx: 2, dy: 0 }, 3),
2351 Terminal::Arm,
2352 ),
2353 to: Endpoint::reference(
2354 Reference::new(box_id, Anchor::Left, Delta { dx: 0, dy: -1 }, -2),
2355 Terminal::Arm,
2356 ),
2357 stroke: light(),
2358 };
2359
2360 let reset_from = connector.with_endpoint_reset(End::From);
2362 let Shape::Connector { from, to, .. } = reset_from else {
2363 panic!()
2364 };
2365 let EndpointAt::Reference(ref r_from) = from.at else {
2366 panic!()
2367 };
2368 let EndpointAt::Reference(ref r_to) = to.at else {
2369 panic!()
2370 };
2371 assert_eq!(r_from.offset, Delta::ZERO);
2372 assert_eq!(r_from.out, 0);
2373 assert_eq!(r_to.offset, Delta { dx: 0, dy: -1 });
2374 assert_eq!(r_to.out, -2);
2375
2376 let reset_to = connector.with_endpoint_reset(End::To);
2378 let Shape::Connector { from, to, .. } = reset_to else {
2379 panic!()
2380 };
2381 let EndpointAt::Reference(ref r_from) = from.at else {
2382 panic!()
2383 };
2384 let EndpointAt::Reference(ref r_to) = to.at else {
2385 panic!()
2386 };
2387 assert_eq!(r_from.offset, Delta { dx: 2, dy: 0 });
2388 assert_eq!(r_from.out, 3);
2389 assert_eq!(r_to.offset, Delta::ZERO);
2390 assert_eq!(r_to.out, 0);
2391
2392 let absolute_conn = Shape::Connector {
2394 from: Endpoint::absolute(Pos { x: 1, y: 1 }, Direction::Up, Terminal::Arm),
2395 to: Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Down, Terminal::Arm),
2396 stroke: light(),
2397 };
2398 assert_eq!(absolute_conn.with_endpoint_reset(End::From), absolute_conn);
2399 }
2400
2401 #[test]
2402 fn with_endpoint_terminal_sets_targeted_endpoint_terminal_preserving_other_properties() {
2403 use monospace_core::SymbolStyleName;
2404
2405 let box_id = ShapeId::new(NonZeroU16::new(1).unwrap());
2406 let orig_from = Endpoint::reference(
2407 Reference::new(box_id, Anchor::Top, Delta { dx: 2, dy: 1 }, 3),
2408 Terminal::Arm,
2409 );
2410 let orig_to = Endpoint::absolute(Pos { x: 20, y: 10 }, Direction::Down, Terminal::Arm);
2411 let connector = Shape::Connector {
2412 from: orig_from.clone(),
2413 to: orig_to.clone(),
2414 stroke: light(),
2415 };
2416
2417 let new_from_terminal = Terminal::Symbol {
2418 style: SymbolStyleName::from("solid"),
2419 };
2420 let updated_from = connector.with_endpoint_terminal(End::From, new_from_terminal.clone());
2421 let Shape::Connector { from, to, .. } = &updated_from else {
2422 panic!();
2423 };
2424 assert_eq!(from.terminal, new_from_terminal);
2425 let EndpointAt::Reference(ref r) = from.at else {
2426 panic!();
2427 };
2428 assert_eq!(r.offset, Delta { dx: 2, dy: 1 });
2429 assert_eq!(r.out, 3);
2430 assert_eq!(r.id, box_id);
2431 assert_eq!(r.anchor, Anchor::Top);
2432 assert_eq!(to, &orig_to);
2433
2434 let new_to_terminal = Terminal::Glyph {
2435 glyph: Glyph::new("◆").unwrap(),
2436 };
2437 let updated_to = connector.with_endpoint_terminal(End::To, new_to_terminal.clone());
2438 let Shape::Connector { from, to, .. } = &updated_to else {
2439 panic!();
2440 };
2441 assert_eq!(to.terminal, new_to_terminal);
2442 assert_eq!(
2443 to.at,
2444 EndpointAt::Absolute {
2445 at: Pos { x: 20, y: 10 },
2446 leaving: Direction::Down,
2447 }
2448 );
2449 assert_eq!(from, &orig_from);
2450 }
2451
2452 #[test]
2453 fn with_endpoint_terminal_on_identical_terminal_produces_equal_shape() {
2454 let box_id = ShapeId::new(NonZeroU16::new(1).unwrap());
2455 let connector = Shape::Connector {
2456 from: Endpoint::reference(
2457 Reference::new(box_id, Anchor::Top, Delta { dx: 2, dy: 0 }, 1),
2458 Terminal::Arm,
2459 ),
2460 to: Endpoint::absolute(Pos { x: 10, y: 5 }, Direction::Right, Terminal::Arm),
2461 stroke: light(),
2462 };
2463
2464 assert_eq!(
2465 connector.with_endpoint_terminal(End::From, Terminal::Arm),
2466 connector
2467 );
2468 assert_eq!(
2469 connector.with_endpoint_terminal(End::To, Terminal::Arm),
2470 connector
2471 );
2472 }
2473
2474 #[test]
2475 fn with_endpoint_terminal_on_non_connector_returns_shape_unchanged() {
2476 let box_shape = Shape::Box {
2477 at: Pos { x: 0, y: 0 },
2478 size: Size {
2479 width: 10,
2480 height: 5,
2481 },
2482 stroke: light(),
2483 fill: None,
2484 text: None,
2485 };
2486 let line_shape = Shape::Line {
2487 at: Pos { x: 0, y: 0 },
2488 len: 10,
2489 orientation: Orientation::Horizontal,
2490 stroke: light(),
2491 };
2492
2493 assert_eq!(
2494 box_shape.with_endpoint_terminal(End::From, Terminal::Arm),
2495 box_shape
2496 );
2497 assert_eq!(
2498 box_shape.with_endpoint_terminal(End::To, Terminal::Arm),
2499 box_shape
2500 );
2501 assert_eq!(
2502 line_shape.with_endpoint_terminal(End::From, Terminal::Arm),
2503 line_shape
2504 );
2505 assert_eq!(
2506 line_shape.with_endpoint_terminal(End::To, Terminal::Arm),
2507 line_shape
2508 );
2509 }
2510
2511 #[test]
2512 fn with_endpoint_detached_freezes_reference_at_resolved_pos_and_anchor_leaving() {
2513 use monospace_core::SymbolStyleName;
2514
2515 let mut diagram = Diagram::new();
2516 let box_shape = Shape::Box {
2517 at: Pos { x: 10, y: 10 },
2518 size: Size {
2519 width: 8,
2520 height: 4,
2521 },
2522 stroke: light(),
2523 fill: None,
2524 text: None,
2525 };
2526 let (d, box_id) = diagram.add(box_shape);
2527 diagram = d;
2528
2529 let offset = Delta { dx: 1, dy: 2 };
2530 let out = 3;
2531 let arm_terminal = Terminal::Arm;
2532 let symbol_terminal = Terminal::Symbol {
2533 style: SymbolStyleName::from("solid"),
2534 };
2535
2536 let arm_ref_endpoint = Endpoint::reference(
2537 Reference::new(box_id, Anchor::Right, offset, out),
2538 arm_terminal.clone(),
2539 );
2540 let symbol_ref_endpoint = Endpoint::reference(
2541 Reference::new(box_id, Anchor::Right, offset, out),
2542 symbol_terminal.clone(),
2543 );
2544
2545 let resolved_arm_pos = arm_ref_endpoint.resolve(&diagram).unwrap();
2546 let resolved_symbol_pos = symbol_ref_endpoint.resolve(&diagram).unwrap();
2547
2548 assert_eq!(resolved_symbol_pos.x, resolved_arm_pos.x + 1);
2549 assert_eq!(resolved_symbol_pos.y, resolved_arm_pos.y);
2550
2551 let connector = Shape::Connector {
2552 from: arm_ref_endpoint.clone(),
2553 to: symbol_ref_endpoint.clone(),
2554 stroke: light(),
2555 };
2556
2557 let detached_from = connector.with_endpoint_detached(End::From, &diagram);
2558 match detached_from {
2559 Shape::Connector { from, to, stroke } => {
2560 assert_eq!(
2561 from,
2562 Endpoint::absolute(resolved_arm_pos, Direction::Right, arm_terminal)
2563 );
2564 assert_eq!(to, symbol_ref_endpoint);
2565 assert_eq!(stroke, light());
2566 }
2567 _ => unreachable!(),
2568 }
2569
2570 let detached_to = connector.with_endpoint_detached(End::To, &diagram);
2571 match detached_to {
2572 Shape::Connector { from, to, stroke } => {
2573 assert_eq!(from, arm_ref_endpoint);
2574 assert_eq!(
2575 to,
2576 Endpoint::absolute(resolved_symbol_pos, Direction::Right, symbol_terminal)
2577 );
2578 assert_eq!(stroke, light());
2579 }
2580 _ => unreachable!(),
2581 }
2582 }
2583
2584 #[test]
2585 fn with_endpoint_detached_on_absolute_or_unresolved_or_non_connector_is_unchanged() {
2586 let diagram = Diagram::new();
2587 let missing_id = ShapeId::new(NonZeroU16::new(99).unwrap());
2588
2589 let connector = Shape::Connector {
2590 from: Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Up, Terminal::Arm),
2591 to: Endpoint::reference(
2592 Reference::new(missing_id, Anchor::Left, Delta::ZERO, 0),
2593 Terminal::Arm,
2594 ),
2595 stroke: light(),
2596 };
2597
2598 assert_eq!(
2599 connector.with_endpoint_detached(End::From, &diagram),
2600 connector
2601 );
2602
2603 assert_eq!(
2604 connector.with_endpoint_detached(End::To, &diagram),
2605 connector
2606 );
2607
2608 let box_shape = Shape::Box {
2609 at: Pos { x: 0, y: 0 },
2610 size: Size {
2611 width: 10,
2612 height: 5,
2613 },
2614 stroke: light(),
2615 fill: None,
2616 text: None,
2617 };
2618 assert_eq!(
2619 box_shape.with_endpoint_detached(End::From, &diagram),
2620 box_shape
2621 );
2622 assert_eq!(
2623 box_shape.with_endpoint_detached(End::To, &diagram),
2624 box_shape
2625 );
2626 }
2627
2628 #[test]
2629 fn with_endpoint_attached_replaces_a_detached_endpoint_and_preserves_its_terminal() {
2630 use monospace_core::SymbolStyleName;
2631
2632 let mut diagram = Diagram::new();
2633 let box_shape = Shape::Box {
2634 at: Pos { x: 10, y: 10 },
2635 size: Size {
2636 width: 8,
2637 height: 4,
2638 },
2639 stroke: light(),
2640 fill: None,
2641 text: None,
2642 };
2643 let (d, box_id) = diagram.add(box_shape);
2644 diagram = d;
2645
2646 let custom_terminal = Terminal::Symbol {
2647 style: SymbolStyleName::from("solid"),
2648 };
2649 let detached_endpoint =
2650 Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Left, custom_terminal.clone());
2651 let other_endpoint =
2652 Endpoint::absolute(Pos { x: 20, y: 20 }, Direction::Right, Terminal::Arm);
2653
2654 let connector = Shape::Connector {
2655 from: detached_endpoint.clone(),
2656 to: other_endpoint.clone(),
2657 stroke: light(),
2658 };
2659
2660 let target_ref = Reference::new(box_id, Anchor::Top, Delta { dx: 1, dy: 2 }, 3);
2661
2662 let attached_from = connector.with_endpoint_attached(End::From, target_ref, &diagram);
2663 match attached_from {
2664 Shape::Connector { from, to, stroke } => {
2665 assert_eq!(
2666 from,
2667 Endpoint::reference(target_ref, custom_terminal.clone())
2668 );
2669 assert_eq!(to, other_endpoint);
2670 assert_eq!(stroke, light());
2671 }
2672 _ => unreachable!(),
2673 }
2674
2675 let attached_to = connector.with_endpoint_attached(End::To, target_ref, &diagram);
2676 match attached_to {
2677 Shape::Connector { from, to, stroke } => {
2678 assert_eq!(from, detached_endpoint);
2679 assert_eq!(to, Endpoint::reference(target_ref, Terminal::Arm));
2680 assert_eq!(stroke, light());
2681 }
2682 _ => unreachable!(),
2683 }
2684 }
2685
2686 #[test]
2687 fn with_endpoint_attached_derives_leaving_from_the_anchor() {
2688 let mut diagram = Diagram::new();
2689 let (d, box_id) = diagram.add(Shape::Box {
2690 at: Pos { x: 0, y: 0 },
2691 size: Size {
2692 width: 10,
2693 height: 10,
2694 },
2695 stroke: light(),
2696 fill: None,
2697 text: None,
2698 });
2699 diagram = d;
2700
2701 for (anchor, expected_leaving) in [
2702 (Anchor::Top, Direction::Up),
2703 (Anchor::Right, Direction::Right),
2704 (Anchor::Bottom, Direction::Down),
2705 (Anchor::Left, Direction::Left),
2706 ] {
2707 let connector = Shape::Connector {
2708 from: Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Down, Terminal::Arm),
2709 to: Endpoint::absolute(Pos { x: 20, y: 20 }, Direction::Up, Terminal::Arm),
2710 stroke: light(),
2711 };
2712 let reference = Reference::new(box_id, anchor, Delta::ZERO, 0);
2713 let attached = connector.with_endpoint_attached(End::From, reference, &diagram);
2714 let Shape::Connector { from, .. } = attached else {
2715 unreachable!()
2716 };
2717 assert_eq!(from.leaving(), expected_leaving);
2718 assert_eq!(from.at.leaving(), expected_leaving);
2719 }
2720 }
2721
2722 #[test]
2723 fn with_endpoint_attached_on_a_line_target_uses_its_anchor() {
2724 let mut diagram = Diagram::new();
2725 let (d, line_id) = diagram.add(Shape::Line {
2726 at: Pos { x: 4, y: 4 },
2727 len: 12,
2728 orientation: Orientation::Horizontal,
2729 stroke: light(),
2730 });
2731 diagram = d;
2732
2733 let to_endpoint = Endpoint::absolute(Pos { x: 10, y: 10 }, Direction::Down, Terminal::Arm);
2734 let connector = Shape::Connector {
2735 from: Endpoint::absolute(Pos { x: 0, y: 0 }, Direction::Up, Terminal::Arm),
2736 to: to_endpoint.clone(),
2737 stroke: light(),
2738 };
2739
2740 let target_ref = Reference::new(line_id, Anchor::Bottom, Delta::ZERO, 0);
2741 let attached = connector.with_endpoint_attached(End::From, target_ref, &diagram);
2742
2743 match attached {
2744 Shape::Connector { from, to, stroke } => {
2745 assert_eq!(from, Endpoint::reference(target_ref, Terminal::Arm));
2746 assert_eq!(from.resolve(&diagram), Some(Pos { x: 9, y: 4 }));
2747 assert_eq!(to, to_endpoint);
2748 assert_eq!(stroke, light());
2749 }
2750 _ => unreachable!(),
2751 }
2752 }
2753
2754 #[test]
2755 fn with_endpoint_attached_on_an_attached_endpoint_is_unchanged() {
2756 let mut diagram = Diagram::new();
2757 let (d1, box1) = diagram.add(Shape::Box {
2758 at: Pos { x: 0, y: 0 },
2759 size: Size {
2760 width: 6,
2761 height: 4,
2762 },
2763 stroke: light(),
2764 fill: None,
2765 text: None,
2766 });
2767 let (d2, box2) = d1.add(Shape::Box {
2768 at: Pos { x: 20, y: 0 },
2769 size: Size {
2770 width: 6,
2771 height: 4,
2772 },
2773 stroke: light(),
2774 fill: None,
2775 text: None,
2776 });
2777 diagram = d2;
2778
2779 let attached_endpoint = Endpoint::reference(
2780 Reference::new(box1, Anchor::Right, Delta::ZERO, 0),
2781 Terminal::Arm,
2782 );
2783 let connector = Shape::Connector {
2784 from: attached_endpoint.clone(),
2785 to: attached_endpoint.clone(),
2786 stroke: light(),
2787 };
2788
2789 let target_ref = Reference::new(box2, Anchor::Left, Delta::ZERO, 0);
2790 assert_eq!(
2791 connector.with_endpoint_attached(End::From, target_ref, &diagram),
2792 connector
2793 );
2794 assert_eq!(
2795 connector.with_endpoint_attached(End::To, target_ref, &diagram),
2796 connector
2797 );
2798 }
2799
2800 #[test]
2801 fn with_endpoint_attached_on_a_missing_or_connector_target_is_unchanged() {
2802 let mut diagram = Diagram::new();
2803 let (d, other_connector_id) = diagram.add(Shape::Connector {
2804 from: Endpoint::absolute(Pos { x: 0, y: 0 }, Direction::Right, Terminal::Arm),
2805 to: Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Left, Terminal::Arm),
2806 stroke: light(),
2807 });
2808 diagram = d;
2809
2810 let connector = Shape::Connector {
2811 from: Endpoint::absolute(Pos { x: 1, y: 1 }, Direction::Right, Terminal::Arm),
2812 to: Endpoint::absolute(Pos { x: 6, y: 6 }, Direction::Left, Terminal::Arm),
2813 stroke: light(),
2814 };
2815
2816 let missing_id = ShapeId::new(NonZeroU16::new(99).unwrap());
2817 let missing_ref = Reference::new(missing_id, Anchor::Top, Delta::ZERO, 0);
2818 let connector_ref = Reference::new(other_connector_id, Anchor::Top, Delta::ZERO, 0);
2819
2820 assert_eq!(
2821 connector.with_endpoint_attached(End::From, missing_ref, &diagram),
2822 connector
2823 );
2824 assert_eq!(
2825 connector.with_endpoint_attached(End::To, missing_ref, &diagram),
2826 connector
2827 );
2828 assert_eq!(
2829 connector.with_endpoint_attached(End::From, connector_ref, &diagram),
2830 connector
2831 );
2832 assert_eq!(
2833 connector.with_endpoint_attached(End::To, connector_ref, &diagram),
2834 connector
2835 );
2836 }
2837
2838 #[test]
2839 fn with_endpoint_attached_on_non_connector_is_unchanged() {
2840 let mut diagram = Diagram::new();
2841 let (d, box_id) = diagram.add(Shape::Box {
2842 at: Pos { x: 0, y: 0 },
2843 size: Size {
2844 width: 10,
2845 height: 5,
2846 },
2847 stroke: light(),
2848 fill: None,
2849 text: None,
2850 });
2851 diagram = d;
2852
2853 let target_ref = Reference::new(box_id, Anchor::Top, Delta::ZERO, 0);
2854
2855 let box_shape = Shape::Box {
2856 at: Pos { x: 1, y: 1 },
2857 size: Size {
2858 width: 4,
2859 height: 4,
2860 },
2861 stroke: light(),
2862 fill: None,
2863 text: None,
2864 };
2865 assert_eq!(
2866 box_shape.with_endpoint_attached(End::From, target_ref, &diagram),
2867 box_shape
2868 );
2869 assert_eq!(
2870 box_shape.with_endpoint_attached(End::To, target_ref, &diagram),
2871 box_shape
2872 );
2873
2874 let line_shape = Shape::Line {
2875 at: Pos { x: 2, y: 2 },
2876 len: 5,
2877 orientation: Orientation::Horizontal,
2878 stroke: light(),
2879 };
2880 assert_eq!(
2881 line_shape.with_endpoint_attached(End::From, target_ref, &diagram),
2882 line_shape
2883 );
2884 assert_eq!(
2885 line_shape.with_endpoint_attached(End::To, target_ref, &diagram),
2886 line_shape
2887 );
2888 }
2889
2890 #[test]
2891 fn with_endpoint_attached_is_idempotent() {
2892 let mut diagram = Diagram::new();
2893 let (d1, box1) = diagram.add(Shape::Box {
2894 at: Pos { x: 0, y: 0 },
2895 size: Size {
2896 width: 8,
2897 height: 4,
2898 },
2899 stroke: light(),
2900 fill: None,
2901 text: None,
2902 });
2903 let (d2, box2) = d1.add(Shape::Box {
2904 at: Pos { x: 20, y: 0 },
2905 size: Size {
2906 width: 8,
2907 height: 4,
2908 },
2909 stroke: light(),
2910 fill: None,
2911 text: None,
2912 });
2913 diagram = d2;
2914
2915 let connector = Shape::Connector {
2916 from: Endpoint::absolute(Pos { x: 5, y: 5 }, Direction::Right, Terminal::Arm),
2917 to: Endpoint::absolute(Pos { x: 15, y: 5 }, Direction::Left, Terminal::Arm),
2918 stroke: light(),
2919 };
2920
2921 let ref1 = Reference::new(box1, Anchor::Right, Delta::ZERO, 0);
2922 let ref2 = Reference::new(box2, Anchor::Left, Delta::ZERO, 0);
2923
2924 let attached_once = connector.with_endpoint_attached(End::From, ref1, &diagram);
2925 let attached_twice = attached_once.with_endpoint_attached(End::From, ref1, &diagram);
2926 assert_eq!(attached_once, attached_twice);
2927
2928 let reattached = attached_once.with_endpoint_attached(End::From, ref2, &diagram);
2930 assert_eq!(attached_once, reattached);
2931 }
2932
2933 #[test]
2934 fn with_endpoint_leaving_updates_detached_endpoint_and_ignores_attached_or_non_connector() {
2935 let box_id = ShapeId::new(NonZeroU16::new(1).unwrap());
2936 let attached_endpoint = Endpoint::reference(
2937 Reference::new(box_id, Anchor::Left, Delta::ZERO, 0),
2938 Terminal::Arm,
2939 );
2940 let detached_endpoint =
2941 Endpoint::absolute(Pos { x: 10, y: 5 }, Direction::Right, Terminal::Arm);
2942
2943 let connector = Shape::Connector {
2944 from: detached_endpoint.clone(),
2945 to: attached_endpoint.clone(),
2946 stroke: light(),
2947 };
2948
2949 let updated_from = connector.with_endpoint_leaving(End::From, Direction::Down);
2951 match updated_from {
2952 Shape::Connector { from, to, stroke } => {
2953 assert_eq!(
2954 from,
2955 Endpoint::absolute(Pos { x: 10, y: 5 }, Direction::Down, Terminal::Arm)
2956 );
2957 assert_eq!(to, attached_endpoint);
2958 assert_eq!(stroke, light());
2959 }
2960 _ => unreachable!(),
2961 }
2962
2963 assert_eq!(
2965 connector.with_endpoint_leaving(End::From, Direction::Right),
2966 connector
2967 );
2968
2969 assert_eq!(
2971 connector.with_endpoint_leaving(End::To, Direction::Up),
2972 connector
2973 );
2974
2975 let connector2 = Shape::Connector {
2977 from: attached_endpoint.clone(),
2978 to: detached_endpoint.clone(),
2979 stroke: light(),
2980 };
2981 let updated_to = connector2.with_endpoint_leaving(End::To, Direction::Left);
2982 match updated_to {
2983 Shape::Connector { from, to, stroke } => {
2984 assert_eq!(from, attached_endpoint);
2985 assert_eq!(
2986 to,
2987 Endpoint::absolute(Pos { x: 10, y: 5 }, Direction::Left, Terminal::Arm)
2988 );
2989 assert_eq!(stroke, light());
2990 }
2991 _ => unreachable!(),
2992 }
2993
2994 let box_shape = Shape::Box {
2996 at: Pos { x: 0, y: 0 },
2997 size: Size {
2998 width: 10,
2999 height: 5,
3000 },
3001 stroke: light(),
3002 fill: None,
3003 text: None,
3004 };
3005 assert_eq!(
3006 box_shape.with_endpoint_leaving(End::From, Direction::Up),
3007 box_shape
3008 );
3009 assert_eq!(
3010 box_shape.with_endpoint_leaving(End::To, Direction::Up),
3011 box_shape
3012 );
3013 }
3014}