egui_map/map/objects.rs
1//! Data types consumed by the [`Map`](super::Map) widget.
2//!
3//! This module contains the geometry primitives ([`RawPoint`], [`RawLine`]),
4//! the map content types ([`MapPoint`], [`MapSegment`], [`MapLabel`]) and the
5//! customization points of the widget: [`MapSettings`],
6//! [`Style`](super::theme::Style), [`VisibilitySetting`],
7//! [`ContextMenuManager`] and [`NodeTemplate`]. The color palette a `Style`
8//! paints with lives in [`super::theme`], via [`MapTheme`](super::theme::MapTheme).
9
10use crate::map::theme::{ColorMode, Style, Theme};
11use egui::{Align2, Color32, FontFamily, FontId, Painter, Pos2, Ui};
12use rstar::AABB;
13use std::convert::{From, Into};
14use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub};
15use std::time::Instant;
16
17/// A point (or vector) in 2D map coordinates.
18///
19/// `RawPoint` supports component-wise arithmetic: [`Mul`], [`Div`],
20/// [`MulAssign`] and [`DivAssign`] with `f32` and the common integer types, and
21/// [`Add`]/[`Sub`] with other points (by value or by reference). It also
22/// converts from and to `[f32; 2]`, integer arrays and [`egui::Pos2`].
23///
24/// # Examples
25///
26/// ```
27/// use egui_map::map::objects::RawPoint;
28///
29/// let a = RawPoint::new(1.0, 2.0);
30/// let b = RawPoint::new(3.0, -1.0);
31///
32/// assert_eq!((a + b).components, [4.0, 1.0]);
33/// assert_eq!((a * 2.0f32).components, [2.0, 4.0]);
34/// ```
35#[derive(Copy, Clone, Debug, PartialEq)]
36pub struct RawPoint {
37 /// The `x` and `y` components of the point.
38 pub components: [f32; 2],
39}
40
41impl RawPoint {
42 /// Creates a point from its `x` and `y` coordinates.
43 pub fn new(x: f32, y: f32) -> Self {
44 Self { components: [x, y] }
45 }
46}
47
48impl rstar::Point for RawPoint {
49 type Scalar = f32;
50 const DIMENSIONS: usize = 2;
51
52 fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self {
53 let mut components = [0.0; 2];
54 for (i, component) in components.iter_mut().enumerate() {
55 *component = generator(i);
56 }
57 Self { components }
58 }
59
60 fn nth(&self, index: usize) -> Self::Scalar {
61 self.components[index]
62 }
63
64 fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar {
65 &mut self.components[index]
66 }
67}
68
69impl Default for RawPoint {
70 fn default() -> Self {
71 Self::new(0.00, 0.00)
72 }
73}
74
75impl Mul<i64> for RawPoint {
76 type Output = Self;
77
78 fn mul(self, rhs: i64) -> Self::Output {
79 Self {
80 components: [
81 self.components[0] * rhs as f32,
82 self.components[1] * rhs as f32,
83 ],
84 }
85 }
86}
87
88impl Mul<i32> for RawPoint {
89 type Output = Self;
90
91 fn mul(self, rhs: i32) -> Self::Output {
92 Self {
93 components: [
94 self.components[0] * rhs as f32,
95 self.components[1] * rhs as f32,
96 ],
97 }
98 }
99}
100
101impl Mul<u64> for RawPoint {
102 type Output = Self;
103
104 fn mul(self, rhs: u64) -> Self::Output {
105 Self {
106 components: [
107 self.components[0] * rhs as f32,
108 self.components[1] * rhs as f32,
109 ],
110 }
111 }
112}
113
114impl Mul<u32> for RawPoint {
115 type Output = Self;
116
117 fn mul(self, rhs: u32) -> Self::Output {
118 Self {
119 components: [
120 self.components[0] * rhs as f32,
121 self.components[1] * rhs as f32,
122 ],
123 }
124 }
125}
126
127impl Mul<f32> for RawPoint {
128 type Output = Self;
129
130 fn mul(self, rhs: f32) -> Self::Output {
131 Self {
132 components: [self.components[0] * rhs, self.components[1] * rhs],
133 }
134 }
135}
136
137impl MulAssign<i64> for RawPoint {
138 fn mul_assign(&mut self, rhs: i64) {
139 self.components[0] = self.components[0] * rhs as f32;
140 self.components[1] = self.components[1] * rhs as f32;
141 }
142}
143
144impl MulAssign<i32> for RawPoint {
145 fn mul_assign(&mut self, rhs: i32) {
146 self.components[0] = self.components[0] * rhs as f32;
147 self.components[1] = self.components[1] * rhs as f32;
148 }
149}
150
151impl MulAssign<u64> for RawPoint {
152 fn mul_assign(&mut self, rhs: u64) {
153 self.components[0] = self.components[0] * rhs as f32;
154 self.components[1] = self.components[1] * rhs as f32;
155 }
156}
157
158impl MulAssign<u32> for RawPoint {
159 fn mul_assign(&mut self, rhs: u32) {
160 self.components[0] = self.components[0] * rhs as f32;
161 self.components[1] = self.components[1] * rhs as f32;
162 }
163}
164
165impl MulAssign<f32> for RawPoint {
166 fn mul_assign(&mut self, rhs: f32) {
167 self.components[0] = self.components[0] * rhs;
168 self.components[1] = self.components[1] * rhs;
169 }
170}
171
172impl Div<i64> for RawPoint {
173 type Output = Self;
174
175 fn div(self, rhs: i64) -> Self::Output {
176 Self {
177 components: [
178 self.components[0] / rhs as f32,
179 self.components[1] / rhs as f32,
180 ],
181 }
182 }
183}
184
185impl Div<i32> for RawPoint {
186 type Output = Self;
187
188 fn div(self, rhs: i32) -> Self::Output {
189 Self {
190 components: [
191 self.components[0] / rhs as f32,
192 self.components[1] / rhs as f32,
193 ],
194 }
195 }
196}
197
198impl Div<u64> for RawPoint {
199 type Output = Self;
200
201 fn div(self, rhs: u64) -> Self::Output {
202 Self {
203 components: [
204 self.components[0] / rhs as f32,
205 self.components[1] / rhs as f32,
206 ],
207 }
208 }
209}
210
211impl Div<u32> for RawPoint {
212 type Output = Self;
213
214 fn div(self, rhs: u32) -> Self::Output {
215 Self {
216 components: [
217 self.components[0] / rhs as f32,
218 self.components[1] / rhs as f32,
219 ],
220 }
221 }
222}
223
224impl Div<f32> for RawPoint {
225 type Output = Self;
226
227 fn div(self, rhs: f32) -> Self::Output {
228 Self {
229 components: [self.components[0] / rhs, self.components[1] / rhs],
230 }
231 }
232}
233
234impl DivAssign<i64> for RawPoint {
235 fn div_assign(&mut self, rhs: i64) {
236 self.components[0] = self.components[0] / rhs as f32;
237 self.components[1] = self.components[1] / rhs as f32;
238 }
239}
240
241impl DivAssign<i32> for RawPoint {
242 fn div_assign(&mut self, rhs: i32) {
243 self.components[0] = self.components[0] / rhs as f32;
244 self.components[1] = self.components[1] / rhs as f32;
245 }
246}
247
248impl DivAssign<u64> for RawPoint {
249 fn div_assign(&mut self, rhs: u64) {
250 self.components[0] = self.components[0] / rhs as f32;
251 self.components[1] = self.components[1] / rhs as f32;
252 }
253}
254
255impl DivAssign<u32> for RawPoint {
256 fn div_assign(&mut self, rhs: u32) {
257 self.components[0] = self.components[0] / rhs as f32;
258 self.components[1] = self.components[1] / rhs as f32;
259 }
260}
261
262impl DivAssign<f32> for RawPoint {
263 fn div_assign(&mut self, rhs: f32) {
264 self.components[0] = self.components[0] / rhs;
265 self.components[1] = self.components[1] / rhs;
266 }
267}
268
269impl Add<RawPoint> for RawPoint {
270 type Output = RawPoint;
271 fn add(self, rhs: RawPoint) -> Self::Output {
272 Self {
273 components: [
274 self.components[0] + rhs.components[0],
275 self.components[1] + rhs.components[1],
276 ],
277 }
278 }
279}
280
281impl Sub<RawPoint> for RawPoint {
282 type Output = RawPoint;
283 fn sub(self, rhs: RawPoint) -> Self::Output {
284 Self {
285 components: [
286 self.components[0] - rhs.components[0],
287 self.components[1] - rhs.components[1],
288 ],
289 }
290 }
291}
292
293impl Add<&RawPoint> for RawPoint {
294 type Output = RawPoint;
295 fn add(self, rhs: &RawPoint) -> Self::Output {
296 Self {
297 components: [
298 self.components[0] + rhs.components[0],
299 self.components[1] + rhs.components[1],
300 ],
301 }
302 }
303}
304
305impl Sub<&RawPoint> for RawPoint {
306 type Output = RawPoint;
307 fn sub(self, rhs: &RawPoint) -> Self::Output {
308 Self {
309 components: [
310 self.components[0] - rhs.components[0],
311 self.components[1] - rhs.components[1],
312 ],
313 }
314 }
315}
316
317impl From<[f32; 2]> for RawPoint {
318 fn from(value: [f32; 2]) -> Self {
319 Self { components: value }
320 }
321}
322
323impl From<Pos2> for RawPoint {
324 fn from(value: Pos2) -> Self {
325 Self {
326 components: [value.x, value.y],
327 }
328 }
329}
330
331impl From<[i64; 2]> for RawPoint {
332 fn from(value: [i64; 2]) -> Self {
333 Self {
334 components: [value[0] as f32, value[1] as f32],
335 }
336 }
337}
338
339impl From<[i32; 2]> for RawPoint {
340 fn from(value: [i32; 2]) -> Self {
341 Self {
342 components: [value[0] as f32, value[1] as f32],
343 }
344 }
345}
346
347impl From<[i16; 2]> for RawPoint {
348 fn from(value: [i16; 2]) -> Self {
349 Self {
350 components: [value[0] as f32, value[1] as f32],
351 }
352 }
353}
354
355impl From<[i8; 2]> for RawPoint {
356 fn from(value: [i8; 2]) -> Self {
357 Self {
358 components: [value[0] as f32, value[1] as f32],
359 }
360 }
361}
362
363impl From<RawPoint> for [f32; 2] {
364 fn from(val: RawPoint) -> Self {
365 [val.components[0], val.components[1]]
366 }
367}
368
369impl From<RawPoint> for Pos2 {
370 fn from(val: RawPoint) -> Self {
371 Pos2::from(val.components)
372 }
373}
374
375/// A straight line segment between two [`RawPoint`]s.
376#[derive(Copy, Clone, Debug)]
377pub struct RawLine {
378 /// The two end points of the segment.
379 pub points: [RawPoint; 2],
380}
381
382impl RawLine {
383 /// Creates a segment between `a` and `b`.
384 pub fn new(a: RawPoint, b: RawPoint) -> Self {
385 Self { points: [a, b] }
386 }
387
388 /// Returns the Euclidean distance between the two end points.
389 pub fn distance(self) -> f32 {
390 let x = self.points[0].components[0] - self.points[1].components[0];
391 let y = self.points[0].components[1] - self.points[1].components[1];
392 (x.powi(2) + y.powi(2)).sqrt()
393 }
394
395 /// Returns the point halfway between the two end points.
396 pub fn midpoint(self) -> RawPoint {
397 let x = (self.points[0].components[0] + self.points[1].components[0]) / 2.0;
398 let y = (self.points[0].components[1] + self.points[1].components[1]) / 2.0;
399 RawPoint::new(x, y)
400 }
401
402 /// Returns the Euclidean distance from `point` to the closest point on
403 /// this segment.
404 ///
405 /// The closest point is the perpendicular projection of `point` onto the
406 /// segment's supporting line, clamped to the segment itself; for a
407 /// zero-length segment it is simply the distance to the endpoint.
408 ///
409 /// # Examples
410 ///
411 /// ```
412 /// use egui_map::map::objects::{RawLine, RawPoint};
413 ///
414 /// let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
415 /// assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
416 /// // Beyond the end of the segment, the endpoint is the closest point.
417 /// assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
418 /// ```
419 pub fn distance_to_point(self, point: RawPoint) -> f32 {
420 let [a, b] = self.points;
421 let ab = b - a;
422 let ap = point - a;
423 let len_sq = ab.components[0].powi(2) + ab.components[1].powi(2);
424 if len_sq == 0.0 {
425 // Degenerate segment: both endpoints coincide.
426 return (ap.components[0].powi(2) + ap.components[1].powi(2)).sqrt();
427 }
428 let t = ((ap.components[0] * ab.components[0] + ap.components[1] * ab.components[1])
429 / len_sq)
430 .clamp(0.0, 1.0);
431 let closest = a + ab * t;
432 let d = point - closest;
433 (d.components[0].powi(2) + d.components[1].powi(2)).sqrt()
434 }
435}
436
437impl From<RawLine> for [Pos2; 2] {
438 fn from(val: RawLine) -> Self {
439 let position1 = val.points[0].into();
440 let position2 = val.points[1].into();
441 [position1, position2]
442 }
443}
444
445impl From<[[i64; 2]; 2]> for RawLine {
446 fn from(value: [[i64; 2]; 2]) -> Self {
447 Self {
448 points: [RawPoint::from(value[0]), RawPoint::from(value[1])],
449 }
450 }
451}
452
453/// A free-floating text label drawn on the map.
454///
455/// Labels are installed with [`Map::add_labels`](super::Map::add_labels).
456#[derive(Clone, Debug)]
457pub struct MapLabel {
458 /// The text to display.
459 pub text: String,
460 /// The position of the label's center.
461 pub center: Pos2,
462}
463
464impl Default for MapLabel {
465 fn default() -> Self {
466 MapLabel::new()
467 }
468}
469
470impl MapLabel {
471 /// Creates an empty label centered at the origin.
472 pub fn new() -> Self {
473 MapLabel {
474 text: String::new(),
475 center: Pos2::new(0.00, 0.00),
476 }
477 }
478}
479
480/// A connection line between two points on the map, ready to be stored in an
481/// [`rstar::RTree`].
482///
483/// Mirrors `sde::objects::SdeSegment`'s shape (`id`, `point1`, `point2`) so
484/// callers that already hold `sde` connection data can build one with a
485/// straight field-for-field copy; the only structural difference is `f32`
486/// instead of `f64` for the coordinates, matching `egui`'s own coordinate
487/// type (`egui` — and therefore this widget — doesn't work in `f64`).
488///
489/// Lines are installed with [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines),
490/// keyed by an id that nodes reference through [`MapPoint::connections`].
491/// The bounding box the R-tree uses for broad-phase viewport culling and
492/// hit-testing is computed on demand from `point1`/`point2` in
493/// [`envelope`](rstar::RTreeObject::envelope) rather than cached on the
494/// struct, same as `SdeSegment`.
495#[derive(Clone, Copy, Debug, PartialEq)]
496pub struct MapSegment {
497 /// Identifier shared with the line key (and with the
498 /// [`MapPoint::connections`] of the endpoint nodes).
499 pub id: (usize, usize),
500 /// One endpoint of the segment, in map coordinates.
501 pub point1: [f32; 2],
502 /// The other endpoint of the segment, in map coordinates.
503 pub point2: [f32; 2],
504}
505
506impl MapSegment {
507 /// Creates a segment for `id` between `point1` and `point2`.
508 pub fn new(id: (usize, usize), point1: [f32; 2], point2: [f32; 2]) -> Self {
509 Self { id, point1, point2 }
510 }
511
512 /// The segment geometry as a [`RawLine`], for the distance/midpoint math
513 /// callers already get from that type.
514 pub(crate) fn raw_line(&self) -> RawLine {
515 RawLine::new(RawPoint::from(self.point1), RawPoint::from(self.point2))
516 }
517}
518
519impl rstar::RTreeObject for MapSegment {
520 type Envelope = AABB<[f32; 2]>;
521
522 fn envelope(&self) -> Self::Envelope {
523 AABB::from_corners(
524 [
525 self.point1[0].min(self.point2[0]),
526 self.point1[1].min(self.point2[1]),
527 ],
528 [
529 self.point1[0].max(self.point2[0]),
530 self.point1[1].max(self.point2[1]),
531 ],
532 )
533 }
534}
535
536/// A node on the map: an id, a 2D position and an optional display name.
537///
538/// Mirrors `sde::objects::SdePoint`'s shape (public `coords`/`id`/`name`/
539/// `connections` fields) so callers that already hold `sde` map-query data
540/// can build one with a straight field-for-field copy. Structural
541/// differences from `SdePoint`:
542///
543/// - `f32` instead of `f64` for `coords`, matching `egui`'s own coordinate
544/// type.
545/// - 2 components instead of 3: this widget only ever renders a 2D map, so
546/// there's no third axis to carry.
547/// - `id` is a plain `usize`, not `Option<usize>`. `SdePoint` uses `None`
548/// for a bare coordinate with no entity behind it (e.g. a bounding-box
549/// corner); every `MapPoint` loaded into the widget represents a real,
550/// placed node whose id is used directly as the point-set `HashMap` key
551/// and the kd-tree payload (see [`Map::add_hashmap_points`]), so an
552/// optional id would just push an `.unwrap()` (or a silently dropped
553/// node) into those call sites with no caller ever passing `None`.
554///
555/// Nodes are loaded into the widget through
556/// [`Map::add_hashmap_points`](super::Map::add_hashmap_points), keyed by their
557/// id.
558#[derive(Clone, Debug, PartialEq)]
559pub struct MapPoint {
560 /// Position of the node, in map coordinates.
561 pub coords: [f32; 2],
562 /// Node identifier, used for lookups, notifications and markers.
563 pub id: usize,
564 /// Display name shown next to the node; `None` if it was never set.
565 pub name: Option<String>,
566 /// Ids of the lines connecting this node with others.
567 ///
568 /// Each entry must match a key of the map passed to
569 /// [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines) (and
570 /// [`MapSegment::id`]). The usual pattern is to push the same pair into
571 /// the `connections` of **both** endpoint nodes. Line visibility is
572 /// computed from the segment bounding boxes (R-tree), not from node
573 /// visibility, so a line is drawn whenever its bounding box intersects
574 /// the viewport.
575 pub connections: Vec<(usize, usize)>,
576 /// Persistent fill color for this node's default circle, in place of
577 /// [`NodeStyle::fill_color`](super::NodeStyle::fill_color). `None`
578 /// (the default) keeps today's behavior of every node sharing the
579 /// same style color.
580 ///
581 /// Only consulted by the built-in circle drawn when no
582 /// [`NodeTemplate`] is installed -- a custom template receives this
583 /// same `MapPoint` and decides for itself whether/how to use `color`.
584 pub color: Option<Color32>,
585}
586
587impl MapPoint {
588 /// Creates a node with the given `id` at the given map coordinates.
589 pub fn new(id: usize, coords: [f32; 2]) -> MapPoint {
590 MapPoint {
591 coords,
592 id,
593 connections: Vec::new(),
594 name: None,
595 color: None,
596 }
597 }
598
599 /// Returns the node identifier.
600 pub fn get_id(&self) -> usize {
601 self.id
602 }
603
604 /// Returns the node display name (empty if it was never set).
605 pub fn get_name(&self) -> String {
606 self.name.clone().unwrap_or_default()
607 }
608
609 /// Sets the node display name.
610 pub fn set_name(&mut self, value: String) {
611 self.name = Some(value);
612 }
613}
614
615impl From<std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>> for MapPoint {
616 fn from(value: std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>) -> Self {
617 let k = value.get();
618 k.clone()
619 }
620}
621
622#[derive(Clone)]
623pub(crate) struct MapBounds {
624 pub min: RawPoint,
625 pub max: RawPoint,
626 pub pos: RawPoint,
627 pub dist: f32,
628}
629
630impl MapBounds {
631 pub fn new() -> Self {
632 MapBounds {
633 min: RawPoint::default(),
634 max: RawPoint::default(),
635 pos: RawPoint::default(),
636 dist: 0.0,
637 }
638 }
639}
640
641impl Default for MapBounds {
642 fn default() -> Self {
643 MapBounds::new()
644 }
645}
646
647pub(crate) struct TextSettings {
648 pub position: RawPoint,
649 pub anchor: Align2,
650 pub text: String,
651 pub size: f32,
652 pub family: FontFamily,
653 pub text_color: Color32,
654}
655
656/// Configuration of a [`Map`](super::Map) widget.
657///
658/// [`MapSettings::default()`] provides sensible zoom limits plus a light and a
659/// dark theme; the widget picks the style to apply based on
660/// [`egui::Visuals::dark_mode`], using `styles[0]` in light mode and
661/// `styles[1]` in dark mode.
662#[derive(Clone, Debug)]
663pub struct MapSettings {
664 /// Maximum zoom factor.
665 pub max_zoom: f32,
666 /// Minimum zoom factor.
667 pub min_zoom: f32,
668 /// Zoom threshold above which connection lines become visible.
669 pub line_visible_zoom: f32,
670 /// Zoom threshold above which node names become visible when
671 /// [`node_text_visibility`](Self::node_text_visibility) is
672 /// [`VisibilitySetting::Always`].
673 pub label_visible_zoom: f32,
674 /// Controls when node names are displayed.
675 pub node_text_visibility: VisibilitySetting,
676 /// Effect drawn on nodes registered with
677 /// [`Map::update_marker`](super::Map::update_marker).
678 ///
679 /// Persistent, so it keeps the app repainting for as long as a marker
680 /// exists. Ignored when a [`NodeTemplate`] is installed.
681 ///
682 /// Node *state* set through [`NodeHandle`](super::NodeHandle) picks its own
683 /// effect per node and does not read this field.
684 pub marker_animation: SteadyAnimation,
685 /// Font size, **in screen pixels**, of the node names.
686 ///
687 /// This is a screen-space size: it deliberately does *not* scale with the
688 /// zoom factor, so a name stays exactly as readable when the map is zoomed
689 /// all the way out as when it is zoomed in. Because the nodes pack closer
690 /// together as you zoom out while the names keep their size, names take up
691 /// proportionally more of the view down there — use
692 /// [`label_visible_zoom`](Self::label_visible_zoom) or
693 /// [`node_text_visibility`](Self::node_text_visibility) to control when
694 /// they are worth showing at all.
695 pub node_text_size: f32,
696 /// Font size, **in screen pixels**, of the free-floating [`MapLabel`]s.
697 ///
698 /// Screen-space, exactly like [`node_text_size`](Self::node_text_size).
699 pub label_text_size: f32,
700 /// Per-mode styles; index `0` is used in light mode, index `1` in dark
701 /// mode. Their colors are kept in sync with the active
702 /// [`MapTheme`](super::theme::MapTheme) -- see
703 /// [`Map::set_theme`](super::Map::set_theme) -- rather than set here.
704 pub styles: Vec<Style>,
705}
706
707impl MapSettings {
708 /// Creates settings with all zoom thresholds set to `0.0` and a single
709 /// transparent style.
710 ///
711 /// Prefer [`MapSettings::default()`] unless you really need to build the
712 /// configuration from scratch.
713 pub fn new() -> Self {
714 MapSettings {
715 max_zoom: 0.0,
716 min_zoom: 0.0,
717 line_visible_zoom: 0.0,
718 label_visible_zoom: 0.0,
719 node_text_visibility: VisibilitySetting::Always,
720 marker_animation: SteadyAnimation::Blink,
721 node_text_size: 12.0,
722 label_text_size: 24.0,
723 styles: vec![Style::new()],
724 }
725 }
726}
727
728impl Default for MapSettings {
729 /// Returns the default configuration: zoom from `0.1` to `2.0`, connection
730 /// lines visible above `0.2`, node names above `0.58`, and built-in light
731 /// and dark themes.
732 fn default() -> Self {
733 let mut obj = MapSettings {
734 max_zoom: 2.0,
735 min_zoom: 0.1,
736 line_visible_zoom: 0.2,
737 label_visible_zoom: 0.58,
738 node_text_visibility: VisibilitySetting::Always,
739 marker_animation: SteadyAnimation::Blink,
740 node_text_size: 12.0,
741 label_text_size: 24.0,
742 styles: Vec::new(),
743 };
744
745 // The border/background colors below are placeholders, overwritten
746 // by `Map::assign_visual_style` from egui's own visuals on the first
747 // frame. The node/text/alert/line colors instead come from the
748 // default `MapTheme` (see `Map::set_theme`) so they never duplicate
749 // what `Theme::colors` already defines -- `Map::apply_theme_colors`
750 // keeps them in sync with whichever `MapTheme` is installed.
751 let light = Theme::default().colors(ColorMode::Light);
752 let dark = Theme::default().colors(ColorMode::Dark);
753
754 // light Theme
755 obj.styles.push(Style {
756 border: Some(egui::Stroke {
757 width: 2.0,
758 color: Color32::from_rgb(216, 142, 58),
759 }),
760 line: Some(egui::Stroke {
761 width: 2.0,
762 color: light.segment,
763 }),
764 fill_color: light.node,
765 text_color: light.text,
766 font: Some(FontId::new(12.00, FontFamily::Proportional)),
767 background_color: Color32::WHITE,
768 alert_color: light.alert,
769 });
770
771 // Dark Theme
772 obj.styles.push(Style {
773 border: Some(egui::Stroke {
774 width: 2.0,
775 color: Color32::GOLD,
776 }),
777 line: Some(egui::Stroke {
778 width: 2.0,
779 color: dark.segment,
780 }),
781 fill_color: dark.node,
782 text_color: dark.text,
783 font: Some(FontId::new(12.00, FontFamily::Proportional)),
784 background_color: Color32::DARK_GRAY,
785 alert_color: dark.alert,
786 });
787 obj
788 }
789}
790
791/// A built-in effect that plays once and ends.
792///
793/// Anchored to the [`Instant`] an event happened, these are the animations
794/// reached through [`NodeHandle`](super::NodeHandle): `map.node(id)?.ripple(t)`.
795/// The widget drops the notification and stops repainting once the effect
796/// finishes. See [`crate::map::animation`] for what each looks like and how
797/// long it runs.
798///
799/// Ignored when a [`NodeTemplate`] is installed — the template's
800/// `notification_ui` takes over. The effects stay reachable there through
801/// [`Animation`](crate::map::animation::Animation).
802#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
803pub enum NodeAnimation {
804 /// Expanding, fading disc. Reads as "one thing happened here".
805 #[default]
806 Pulse,
807 /// Three staggered expanding rings. Reads as "activity is ongoing".
808 Ripple,
809 /// A ring that empties clockwise. Reads as "how old is this information".
810 CountdownArc,
811 /// A disc that overshoots its size and settles. For nodes that just appeared.
812 ScaleIn,
813 /// Four ticks converging on the node. Reads as "target acquired".
814 Crosshair,
815}
816
817/// A built-in effect that runs until it is cleared.
818///
819/// Named after how long it lasts rather than after who uses it, because it has
820/// two consumers: node state set through [`NodeHandle`](super::NodeHandle)
821/// (`map.node(id)?.halo()`), and markers registered with
822/// [`Map::update_marker`](super::Map::update_marker), which pick their look
823/// with [`MapSettings::marker_animation`].
824///
825/// These never end, so the widget keeps requesting repaints for as long as one
826/// is active. That is fine for the handful of elements they are meant for, but
827/// it does keep the app redrawing — see [`crate::map::animation`].
828#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
829pub enum SteadyAnimation {
830 /// Thick ring blinking on and off. The long-standing marker look.
831 #[default]
832 Blink,
833 /// Ring whose opacity breathes in and out. Calmer than [`Self::Blink`].
834 Halo,
835 /// A dot circling the node. Reads as "under observation".
836 Orbit,
837}
838
839/// Which endpoint a [`SegmentAnimation::Comet`] pass starts from.
840///
841/// A segment's own endpoint order (`a`, `b` as loaded through
842/// [`Map::add_lines`](super::Map::add_lines)) is not usually meaningful to a
843/// caller — naming the two ends [`Self::Forward`]/[`Self::Reverse`] instead
844/// keeps the choice about the animation's direction, not about internal
845/// storage order.
846#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
847pub enum CometDirection {
848 /// From the segment's first endpoint to its second.
849 #[default]
850 Forward,
851 /// From the segment's second endpoint to its first.
852 Reverse,
853}
854
855/// A built-in effect that plays once and ends, for a segment.
856///
857/// Anchored to the [`Instant`] an event happened, these are the animations
858/// reached through [`SegmentHandle`](super::SegmentHandle):
859/// `map.segment(id)?.flash(t)`. The widget drops the notification and stops
860/// repainting once the effect finishes. See [`crate::map::animation`] for
861/// what each looks like and how long it runs.
862///
863/// Ignored when a [`SegmentTemplate`] is installed — the template's
864/// `segment_notification_ui` takes over. The effect stays reachable there
865/// through [`Animation`](crate::map::animation::Animation).
866#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
867pub enum SegmentAnimation {
868 /// A brief bright flash on the line that fades back out. The segment
869 /// analogue of [`NodeAnimation::Pulse`] — reads as "something happened on
870 /// this route".
871 #[default]
872 FlashDecay,
873 /// A single dot pass from one endpoint to the other, then gone — the
874 /// event-driven counterpart to [`SteadySegmentAnimation::Comet`]. Reads
875 /// as "one thing moved along this route just now", direction included,
876 /// rather than "traffic keeps flowing this way".
877 Comet(CometDirection),
878 /// The line drawing itself in from the first endpoint to the second,
879 /// then gone. Reads as "this route was just established" rather than
880 /// "something travelled along it".
881 Wipe,
882}
883
884/// A built-in effect that runs until it is cleared, for a segment.
885///
886/// Reached through node state set on [`SegmentHandle`](super::SegmentHandle)
887/// (`map.segment(id)?.comet()` / `.dash()`). Like [`SteadyAnimation`], these
888/// never end, so the widget keeps requesting repaints for as long as one is
889/// active — fine for a handful of highlighted routes, not for every segment
890/// on the map.
891#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
892pub enum SteadySegmentAnimation {
893 /// A dot travelling from one endpoint to the other and looping. Reads as
894 /// "this is the direction of flow".
895 #[default]
896 Comet,
897 /// A dashed line whose pattern slides along the segment ("marching
898 /// ants"). Reads as "route", classic for a path someone might follow.
899 Dash,
900 /// A localized band of brightness travelling the length of the segment
901 /// and looping, fading out before it reaches either end rather than
902 /// snapping back. Reads as "flow", softer and less busy than [`Self::Dash`].
903 GlowBand,
904 /// A row of arrow shapes sliding along the segment, pointing the way.
905 /// Reads as "direction of travel", more explicit than [`Self::Comet`]'s
906 /// single dot.
907 Chevrons,
908}
909
910/// Controls when the name of a node is displayed next to it.
911#[derive(Clone, Debug, PartialEq)]
912pub enum VisibilitySetting {
913 /// Never show node names.
914 Hidden,
915 /// Only show the name of the node closest to the mouse pointer.
916 Hover,
917 /// Always show node names, subject to [`MapSettings::label_visible_zoom`].
918 Always,
919}
920
921/// Provides the contents of the widget's right-click context menu.
922///
923/// Install an implementation with
924/// [`Map::set_context_manager`](super::Map::set_context_manager).
925///
926/// # Examples
927///
928/// ```
929/// use egui_map::map::objects::ContextMenuManager;
930///
931/// struct MyMenu;
932///
933/// impl ContextMenuManager for MyMenu {
934/// fn ui(&self, ui: &mut egui::Ui) {
935/// ui.label("Hello from the map!");
936/// }
937/// }
938/// ```
939pub trait ContextMenuManager {
940 /// Builds the menu contents; called every frame while the menu is open.
941 fn ui(&self, ui: &mut Ui);
942}
943
944/// Customizes how nodes and their visual effects are rendered.
945///
946/// When a template is installed with
947/// [`Map::set_node_template`](super::Map::set_node_template), the widget
948/// delegates all node painting to it instead of using the built-in shapes and
949/// animations — including the node name labels, so draw the name yourself in
950/// [`NodeTemplate::node_ui`] if you need it.
951///
952/// The positions passed to these methods are in screen coordinates: already
953/// scaled by `zoom` and translated to the viewport origin. Multiply every size
954/// by `zoom` so your shapes scale together with the map.
955///
956/// # Animation idioms
957///
958/// egui only repaints on demand, so any method that animates (a blinking
959/// marker, a fading notification, ...) must call
960/// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) to keep the
961/// frames coming. Time-driven effects are usually computed from
962/// [`Instant::now()`] (see `initial_time` in
963/// [`NodeTemplate::notification_ui`]) or from the system clock.
964///
965/// # Examples
966///
967/// A node drawn as a rounded box with its name inside, plus a notification
968/// animation that expands and fades out over two seconds:
969///
970/// ```
971/// use egui_map::map::objects::{MapPoint, NodeTemplate, NotificationContext, MarkerContext};
972/// use egui::{Align2, Color32, CornerRadius, FontId, Pos2, Rect, Stroke, Ui, Vec2};
973/// use std::time::Instant;
974///
975/// struct BoxedNodes;
976///
977/// impl NodeTemplate for BoxedNodes {
978/// fn node_ui(&self, ui: &mut Ui, position: Pos2, zoom: f32, point: &MapPoint) {
979/// // Multiply every size by `zoom` so the node scales with the map.
980/// let rect = Rect::from_center_size(position, Vec2::new(90.0 * zoom, 35.0 * zoom));
981/// let rounding = CornerRadius::same((10.0 * zoom) as u8);
982/// let painter = ui.painter();
983/// painter.rect_filled(rect, rounding, ui.visuals().extreme_bg_color);
984/// painter.rect_stroke(
985/// rect,
986/// rounding,
987/// Stroke::new(4.0 * zoom, Color32::WHITE),
988/// egui::StrokeKind::Middle,
989/// );
990/// painter.text(
991/// position,
992/// Align2::CENTER_CENTER,
993/// point.get_name(),
994/// FontId::proportional(12.0 * zoom),
995/// Color32::WHITE,
996/// );
997/// }
998///
999/// fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool {
1000/// let secs = Instant::now().duration_since(ctx.initial_time).as_secs_f32();
1001/// // Expand the stroke and fade the color out over 2 seconds.
1002/// let alpha = (1.0 - secs / 2.0).clamp(0.0, 1.0);
1003/// let fading = Color32::from_rgba_unmultiplied(
1004/// ctx.color.r(),
1005/// ctx.color.g(),
1006/// ctx.color.b(),
1007/// (255.0 * alpha) as u8,
1008/// );
1009/// let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
1010/// ui.painter().rect_stroke(
1011/// rect,
1012/// CornerRadius::same((10.0 * ctx.zoom) as u8),
1013/// Stroke::new((4.0 + 25.0 * secs) * ctx.zoom, fading),
1014/// egui::StrokeKind::Middle,
1015/// );
1016/// // Keep the animation frames coming.
1017/// ui.ctx().request_repaint();
1018/// // Returning `false` removes the notification.
1019/// secs < 2.0
1020/// }
1021/// # fn selection_ui(&self, ui: &mut Ui, point: Pos2, zoom: f32) {
1022/// # let rect = Rect::from_center_size(point, Vec2::new(94.0 * zoom, 39.0 * zoom));
1023/// # ui.painter().rect_stroke(
1024/// # rect,
1025/// # CornerRadius::same((10.0 * zoom) as u8),
1026/// # Stroke::new(3.0 * zoom, Color32::YELLOW),
1027/// # egui::StrokeKind::Middle,
1028/// # );
1029/// # }
1030/// # fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext) {
1031/// # ui.painter().circle_stroke(ctx.position, 6.0 * ctx.zoom, Stroke::new(2.0 * ctx.zoom, Color32::LIGHT_GREEN));
1032/// # ui.ctx().request_repaint();
1033/// # }
1034/// }
1035/// ```
1036///
1037/// # Note on `NodeAnimation`/`SteadyAnimation` in the examples above
1038///
1039/// The hidden (`#`-prefixed) stub methods above still take `Pos2`/`f32`
1040/// directly rather than a context struct -- only [`NotificationContext`] and
1041/// [`MarkerContext`] exist; `node_ui`/`selection_ui` were not wide enough to
1042/// need one.
1043pub trait NodeTemplate {
1044 /// Draws a node, replacing the default filled circle.
1045 ///
1046 /// Called every frame for each visible node. The widget no longer draws
1047 /// the node name once a template is installed, so render it here (e.g.
1048 /// with [`Painter::text`](egui::Painter::text)) if you need it.
1049 fn node_ui(&self, ui: &mut Ui, _viewport_position: Pos2, _zoom: f32, _point: &MapPoint);
1050
1051 /// Draws the highlight over the node closest to the mouse pointer.
1052 ///
1053 /// The nearest node is only computed while the pointer is over the map and
1054 /// [`MapSettings::node_text_visibility`] is [`VisibilitySetting::Hover`].
1055 fn selection_ui(&self, ui: &mut Ui, _viewport_position: Pos2, _zoom: f32);
1056
1057 /// Draws the notification effect of a node notified at
1058 /// `ctx.initial_time`.
1059 ///
1060 /// Called every frame for each node passed to
1061 /// [`Map::notify`](super::Map::notify) or animated through
1062 /// [`Map::node`](super::Map::node)'s event methods (`pulse`, `ripple`,
1063 /// ...). `ctx.kind` is which of those was requested and `ctx.node_id` is
1064 /// the id of the node it belongs to -- use them to dispatch to the
1065 /// matching built-in [`Animation`](crate::map::animation::Animation)
1066 /// function (or your own effect) instead of reimplementing every
1067 /// animation by hand. See [`NotificationContext`] for the rest of the
1068 /// fields. Should return `true` while the animation is still playing —
1069 /// remember to call
1070 /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) —;
1071 /// once it returns `false` the notification is discarded.
1072 fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool;
1073
1074 /// Draws a marker over the given node.
1075 ///
1076 /// Called every frame for two different things -- see [`MarkerContext`]
1077 /// for what `ctx.kind`/`ctx.node_id` mean in each case. For animated
1078 /// markers (e.g. a blinking light), drive the effect from the system
1079 /// clock and call
1080 /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint).
1081 fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext);
1082}
1083
1084/// The context passed to [`NodeTemplate::notification_ui`].
1085///
1086/// `#[non_exhaustive]` so a future field can be added here without another
1087/// breaking change to [`NodeTemplate`].
1088#[derive(Clone, Copy, Debug)]
1089#[non_exhaustive]
1090pub struct NotificationContext {
1091 /// The node's screen position: already scaled by `zoom` and translated
1092 /// to the viewport origin.
1093 pub position: Pos2,
1094 /// Multiply every size you draw by this so it scales with the map.
1095 pub zoom: f32,
1096 /// When the notification started -- usually fed into a progress
1097 /// computation like `Instant::now().duration_since(initial_time)`.
1098 pub initial_time: Instant,
1099 /// The color requested for this notification (the node's own color, or
1100 /// the current style's `alert_color` if none was set).
1101 pub color: Color32,
1102 /// Which built-in event effect was requested (`pulse`, `ripple`, ...).
1103 /// Match on this to dispatch to the corresponding
1104 /// [`Animation`](crate::map::animation::Animation) function instead of
1105 /// reimplementing the lookup yourself.
1106 pub kind: NodeAnimation,
1107 /// The id of the node this notification belongs to.
1108 pub node_id: usize,
1109}
1110
1111/// The context passed to [`NodeTemplate::marker_ui`].
1112///
1113/// `#[non_exhaustive]`, like [`NotificationContext`], so a future field can
1114/// be added here without another breaking change.
1115#[derive(Clone, Copy, Debug)]
1116#[non_exhaustive]
1117pub struct MarkerContext {
1118 /// The node's screen position: already scaled by `zoom` and translated
1119 /// to the viewport origin.
1120 pub position: Pos2,
1121 /// Multiply every size you draw by this so it scales with the map.
1122 pub zoom: f32,
1123 /// Which persistent effect to draw. For a node's own lasting state (set
1124 /// through [`Map::node`](super::Map::node)'s `halo`/`blink`/`orbit`)
1125 /// this is whichever of those was requested; for a plain marker
1126 /// (registered with [`Map::update_marker`](super::Map::update_marker))
1127 /// it is always [`MapSettings::marker_animation`], since every marker
1128 /// shares that one setting. There is no way from inside this hook to
1129 /// tell the two *cases* apart -- only which `SteadyAnimation` to draw
1130 /// for whichever one it is.
1131 pub kind: SteadyAnimation,
1132 /// The id of the node the state/marker belongs to (for a marker: the id
1133 /// it points at, not the marker's own id).
1134 pub node_id: usize,
1135}
1136
1137/// Customizes how segments and their visual effects are rendered.
1138///
1139/// When a template is installed with
1140/// [`Map::set_segment_template`](super::Map::set_segment_template), the widget
1141/// delegates all segment painting to it instead of using the built-in stroke
1142/// and animations.
1143///
1144/// Unlike [`NodeTemplate`], these methods receive a bare [`&Painter`](Painter)
1145/// rather than `&mut Ui`. Segments are visited in bulk, every frame, after the
1146/// R-tree viewport culling in `paint_map_lines`; going through `Ui` would cost
1147/// a layout pass per segment, on top of what the culling already had to
1148/// discard. Use [`Painter::ctx`] to reach the [`egui::Context`] — for example
1149/// to call `request_repaint()`.
1150///
1151/// The positions passed to these methods are in screen coordinates: already
1152/// scaled by `zoom` and translated to the viewport origin, same as
1153/// [`NodeTemplate`]'s. Multiply every size by `zoom` so your shapes scale
1154/// together with the map.
1155///
1156/// # Examples
1157///
1158/// A segment drawn as a dashed line, plus a notification that briefly
1159/// thickens and brightens it:
1160///
1161/// ```
1162/// use egui_map::map::objects::{MapSegment, SegmentTemplate};
1163/// use egui::{Color32, Painter, Pos2, Stroke};
1164/// use std::time::Instant;
1165///
1166/// struct DashedRoutes;
1167///
1168/// impl SegmentTemplate for DashedRoutes {
1169/// fn segment_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, _segment: &MapSegment) {
1170/// // A crude dash: short strokes along the segment, spaced in screen
1171/// // pixels so they don't stretch as the map zooms.
1172/// let dir = b - a;
1173/// let len = dir.length();
1174/// let step = 10.0 * zoom;
1175/// let mut travelled = 0.0;
1176/// while travelled < len {
1177/// let start = a + dir * (travelled / len);
1178/// let end = a + dir * ((travelled + step * 0.6).min(len) / len);
1179/// painter.line_segment([start, end], Stroke::new(2.0 * zoom, Color32::GRAY));
1180/// travelled += step;
1181/// }
1182/// }
1183///
1184/// fn segment_notification_ui(
1185/// &self,
1186/// painter: &Painter,
1187/// a: Pos2,
1188/// b: Pos2,
1189/// zoom: f32,
1190/// initial_time: Instant,
1191/// color: Color32,
1192/// ) -> bool {
1193/// let secs = Instant::now().duration_since(initial_time).as_secs_f32();
1194/// let alpha = (1.0 - secs).clamp(0.0, 1.0);
1195/// let fading =
1196/// Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), (255.0 * alpha) as u8);
1197/// painter.line_segment([a, b], Stroke::new(5.0 * zoom, fading));
1198/// painter.ctx().request_repaint();
1199/// secs < 1.0
1200/// }
1201///
1202/// fn segment_state_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, time: f32, color: Color32) {
1203/// let t = (time / 1.6).rem_euclid(1.0);
1204/// painter.circle_filled(a + (b - a) * t, 4.0 * zoom, color);
1205/// painter.ctx().request_repaint();
1206/// }
1207/// }
1208/// ```
1209pub trait SegmentTemplate {
1210 /// Draws a segment, replacing the default stroked line.
1211 ///
1212 /// Called every frame for each segment that survives the R-tree viewport
1213 /// culling in `paint_map_lines`.
1214 fn segment_ui(
1215 &self,
1216 painter: &Painter,
1217 pos_a: Pos2,
1218 pos_b: Pos2,
1219 zoom: f32,
1220 segment: &MapSegment,
1221 );
1222
1223 /// Draws the notification effect of a segment notified through
1224 /// [`Map::segment`](super::Map::segment).
1225 ///
1226 /// Called every frame for each segment carrying an event-driven effect
1227 /// (see [`SegmentHandle`](super::SegmentHandle)). Should return `true`
1228 /// while the animation is still playing — remember to call
1229 /// [`Painter::ctx`]`().request_repaint()` — once it returns `false` the
1230 /// notification is discarded.
1231 fn segment_notification_ui(
1232 &self,
1233 painter: &Painter,
1234 pos_a: Pos2,
1235 pos_b: Pos2,
1236 zoom: f32,
1237 initial_time: Instant,
1238 color: Color32,
1239 ) -> bool;
1240
1241 /// Draws the lasting state effect of a segment (e.g. a travelling dot).
1242 ///
1243 /// Called every frame for each segment with lasting state set through
1244 /// [`Map::segment`](super::Map::segment). `time` is the frame time in
1245 /// seconds (`ui.input(|i| i.time)`), so every element animated this frame
1246 /// shares one clock. For animated state, remember to call
1247 /// [`Painter::ctx`]`().request_repaint()`.
1248 fn segment_state_ui(
1249 &self,
1250 painter: &Painter,
1251 pos_a: Pos2,
1252 pos_b: Pos2,
1253 zoom: f32,
1254 time: f32,
1255 color: Color32,
1256 );
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261 use super::*;
1262 use std::collections::HashMap;
1263
1264 // ---------- RawPoint ----------
1265
1266 #[test]
1267 fn raw_point_new() {
1268 let p = RawPoint::new(3.5, -2.0);
1269 assert_eq!(p.components, [3.5, -2.0]);
1270 }
1271
1272 // ---------- MapSegment ----------
1273
1274 #[test]
1275 fn map_segment_new_computes_tight_aabb() {
1276 let seg = MapSegment::new((1, 2), [10.0, -5.0], [-2.0, 7.0]);
1277 assert_eq!(seg.id, (1, 2));
1278 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1279 assert_eq!(envelope.lower(), [-2.0, -5.0]);
1280 assert_eq!(envelope.upper(), [10.0, 7.0]);
1281 assert_eq!(seg.raw_line().points[0].components, [10.0, -5.0]);
1282 assert_eq!(seg.raw_line().points[1].components, [-2.0, 7.0]);
1283 }
1284
1285 #[test]
1286 fn map_segment_envelope_returns_its_aabb() {
1287 let seg = MapSegment::new((1, 2), [0.0, 0.0], [4.0, 2.0]);
1288 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1289 assert_eq!(envelope.lower(), [0.0, 0.0]);
1290 assert_eq!(envelope.upper(), [4.0, 2.0]);
1291 }
1292
1293 #[test]
1294 fn map_segment_degenerate_line_has_point_aabb() {
1295 // A zero-length segment must still produce a valid (empty-area) AABB.
1296 let seg = MapSegment::new((1, 2), [3.0, 3.0], [3.0, 3.0]);
1297 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1298 assert_eq!(envelope.lower(), [3.0, 3.0]);
1299 assert_eq!(envelope.upper(), [3.0, 3.0]);
1300 }
1301
1302 #[test]
1303 fn raw_point_default() {
1304 let p = RawPoint::default();
1305 assert_eq!(p.components, [0.0, 0.0]);
1306 }
1307
1308 #[test]
1309 fn raw_point_mul_i64() {
1310 let p = RawPoint::new(2.0, -3.0) * 3i64;
1311 assert_eq!(p.components, [6.0, -9.0]);
1312 }
1313
1314 #[test]
1315 fn raw_point_mul_i32() {
1316 let p = RawPoint::new(2.0, -3.0) * 3i32;
1317 assert_eq!(p.components, [6.0, -9.0]);
1318 }
1319
1320 #[test]
1321 fn raw_point_mul_u64() {
1322 let p = RawPoint::new(2.0, -3.0) * 3u64;
1323 assert_eq!(p.components, [6.0, -9.0]);
1324 }
1325
1326 #[test]
1327 fn raw_point_mul_u32() {
1328 let p = RawPoint::new(2.0, -3.0) * 3u32;
1329 assert_eq!(p.components, [6.0, -9.0]);
1330 }
1331
1332 #[test]
1333 fn raw_point_mul_f32() {
1334 let p = RawPoint::new(2.0, -3.0) * 0.5f32;
1335 assert_eq!(p.components, [1.0, -1.5]);
1336 }
1337
1338 #[test]
1339 fn raw_point_mul_assign_i64() {
1340 let mut p = RawPoint::new(2.0, -3.0);
1341 p *= 3i64;
1342 assert_eq!(p.components, [6.0, -9.0]);
1343 }
1344
1345 #[test]
1346 fn raw_point_mul_assign_i32() {
1347 let mut p = RawPoint::new(2.0, -3.0);
1348 p *= 3i32;
1349 assert_eq!(p.components, [6.0, -9.0]);
1350 }
1351
1352 #[test]
1353 fn raw_point_mul_assign_u64() {
1354 let mut p = RawPoint::new(2.0, -3.0);
1355 p *= 3u64;
1356 assert_eq!(p.components, [6.0, -9.0]);
1357 }
1358
1359 #[test]
1360 fn raw_point_mul_assign_u32() {
1361 let mut p = RawPoint::new(2.0, -3.0);
1362 p *= 3u32;
1363 assert_eq!(p.components, [6.0, -9.0]);
1364 }
1365
1366 #[test]
1367 fn raw_point_mul_assign_f32() {
1368 let mut p = RawPoint::new(2.0, -3.0);
1369 p *= 0.5f32;
1370 assert_eq!(p.components, [1.0, -1.5]);
1371 }
1372
1373 #[test]
1374 fn raw_point_div_i64() {
1375 let p = RawPoint::new(6.0, -9.0) / 3i64;
1376 assert_eq!(p.components, [2.0, -3.0]);
1377 }
1378
1379 #[test]
1380 fn raw_point_div_i32() {
1381 let p = RawPoint::new(6.0, -9.0) / 3i32;
1382 assert_eq!(p.components, [2.0, -3.0]);
1383 }
1384
1385 #[test]
1386 fn raw_point_div_u64() {
1387 let p = RawPoint::new(6.0, -9.0) / 3u64;
1388 assert_eq!(p.components, [2.0, -3.0]);
1389 }
1390
1391 #[test]
1392 fn raw_point_div_u32() {
1393 let p = RawPoint::new(6.0, -9.0) / 3u32;
1394 assert_eq!(p.components, [2.0, -3.0]);
1395 }
1396
1397 #[test]
1398 fn raw_point_div_f32() {
1399 let p = RawPoint::new(1.0, -1.5) / 0.5f32;
1400 assert_eq!(p.components, [2.0, -3.0]);
1401 }
1402
1403 #[test]
1404 fn raw_point_div_assign_i64() {
1405 let mut p = RawPoint::new(6.0, -9.0);
1406 p /= 3i64;
1407 assert_eq!(p.components, [2.0, -3.0]);
1408 }
1409
1410 #[test]
1411 fn raw_point_div_assign_i32() {
1412 let mut p = RawPoint::new(6.0, -9.0);
1413 p /= 3i32;
1414 assert_eq!(p.components, [2.0, -3.0]);
1415 }
1416
1417 #[test]
1418 fn raw_point_div_assign_u64() {
1419 let mut p = RawPoint::new(6.0, -9.0);
1420 p /= 3u64;
1421 assert_eq!(p.components, [2.0, -3.0]);
1422 }
1423
1424 #[test]
1425 fn raw_point_div_assign_u32() {
1426 let mut p = RawPoint::new(6.0, -9.0);
1427 p /= 3u32;
1428 assert_eq!(p.components, [2.0, -3.0]);
1429 }
1430
1431 #[test]
1432 fn raw_point_div_assign_f32() {
1433 let mut p = RawPoint::new(1.0, -1.5);
1434 p /= 0.5f32;
1435 assert_eq!(p.components, [2.0, -3.0]);
1436 }
1437
1438 #[test]
1439 fn raw_point_add() {
1440 let a = RawPoint::new(1.0, 2.0);
1441 let b = RawPoint::new(3.0, -4.0);
1442 let c = a + b;
1443 assert_eq!(c.components, [4.0, -2.0]);
1444 }
1445
1446 #[test]
1447 fn raw_point_sub() {
1448 let a = RawPoint::new(1.0, 2.0);
1449 let b = RawPoint::new(3.0, -4.0);
1450 let c = a - b;
1451 assert_eq!(c.components, [-2.0, 6.0]);
1452 }
1453
1454 #[test]
1455 #[allow(clippy::op_ref)] // se prueba a propósito la impl Add<&RawPoint>
1456 fn raw_point_add_ref() {
1457 let a = RawPoint::new(1.0, 2.0);
1458 let b = RawPoint::new(3.0, -4.0);
1459 let c = a + &b;
1460 assert_eq!(c.components, [4.0, -2.0]);
1461 // b sigue siendo usable tras la suma por referencia
1462 assert_eq!(b.components, [3.0, -4.0]);
1463 }
1464
1465 #[test]
1466 #[allow(clippy::op_ref)] // se prueba a propósito la impl Sub<&RawPoint>
1467 fn raw_point_sub_ref() {
1468 let a = RawPoint::new(1.0, 2.0);
1469 let b = RawPoint::new(3.0, -4.0);
1470 let c = a - &b;
1471 assert_eq!(c.components, [-2.0, 6.0]);
1472 assert_eq!(b.components, [3.0, -4.0]);
1473 }
1474
1475 #[test]
1476 fn raw_point_from_f32_array() {
1477 let p = RawPoint::from([1.5f32, -2.5f32]);
1478 assert_eq!(p.components, [1.5, -2.5]);
1479 }
1480
1481 #[test]
1482 fn raw_point_from_i64_array() {
1483 let p = RawPoint::from([3i64, -4i64]);
1484 assert_eq!(p.components, [3.0, -4.0]);
1485 }
1486
1487 #[test]
1488 fn raw_point_from_i32_array() {
1489 let p = RawPoint::from([3i32, -4i32]);
1490 assert_eq!(p.components, [3.0, -4.0]);
1491 }
1492
1493 #[test]
1494 fn raw_point_from_i16_array() {
1495 let p = RawPoint::from([3i16, -4i16]);
1496 assert_eq!(p.components, [3.0, -4.0]);
1497 }
1498
1499 #[test]
1500 fn raw_point_from_i8_array() {
1501 let p = RawPoint::from([3i8, -4i8]);
1502 assert_eq!(p.components, [3.0, -4.0]);
1503 }
1504
1505 #[test]
1506 fn raw_point_from_pos2() {
1507 let p = RawPoint::from(Pos2::new(7.0, 8.0));
1508 assert_eq!(p.components, [7.0, 8.0]);
1509 }
1510
1511 #[test]
1512 fn raw_point_into_f32_array() {
1513 let arr: [f32; 2] = RawPoint::new(7.0, 8.0).into();
1514 assert_eq!(arr, [7.0, 8.0]);
1515 }
1516
1517 #[test]
1518 fn raw_point_into_pos2() {
1519 let pos: Pos2 = RawPoint::new(7.0, 8.0).into();
1520 assert_eq!(pos, Pos2::new(7.0, 8.0));
1521 }
1522
1523 // ---------- RawLine ----------
1524
1525 #[test]
1526 fn raw_line_new() {
1527 let a = RawPoint::new(1.0, 2.0);
1528 let b = RawPoint::new(3.0, 4.0);
1529 let line = RawLine::new(a, b);
1530 assert_eq!(line.points[0].components, [1.0, 2.0]);
1531 assert_eq!(line.points[1].components, [3.0, 4.0]);
1532 }
1533
1534 #[test]
1535 fn raw_line_distance() {
1536 // triángulo 3-4-5
1537 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(3.0, 4.0));
1538 assert_eq!(line.distance(), 5.0);
1539 }
1540
1541 #[test]
1542 fn raw_line_distance_zero() {
1543 let line = RawLine::new(RawPoint::new(2.0, 2.0), RawPoint::new(2.0, 2.0));
1544 assert_eq!(line.distance(), 0.0);
1545 }
1546
1547 #[test]
1548 fn raw_line_midpoint() {
1549 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(4.0, 6.0));
1550 let mid = line.midpoint();
1551 assert_eq!(mid.components, [2.0, 3.0]);
1552 }
1553
1554 #[test]
1555 fn raw_line_distance_to_point_on_segment() {
1556 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1557 assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
1558 assert_eq!(line.distance_to_point(RawPoint::new(5.0, 0.0)), 0.0);
1559 }
1560
1561 #[test]
1562 fn raw_line_distance_to_point_beyond_endpoints() {
1563 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1564 // Past the end of the segment, the closest point is the endpoint.
1565 assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
1566 assert_eq!(line.distance_to_point(RawPoint::new(-3.0, -4.0)), 5.0);
1567 }
1568
1569 #[test]
1570 fn raw_line_distance_to_point_degenerate_segment() {
1571 let line = RawLine::new(RawPoint::new(1.0, 1.0), RawPoint::new(1.0, 1.0));
1572 assert_eq!(line.distance_to_point(RawPoint::new(4.0, 5.0)), 5.0);
1573 }
1574
1575 #[test]
1576 fn raw_line_into_pos2_array() {
1577 let line = RawLine::new(RawPoint::new(1.0, 2.0), RawPoint::new(3.0, 4.0));
1578 let arr: [Pos2; 2] = line.into();
1579 assert_eq!(arr, [Pos2::new(1.0, 2.0), Pos2::new(3.0, 4.0)]);
1580 }
1581
1582 #[test]
1583 fn raw_line_from_i64_arrays() {
1584 let line = RawLine::from([[1i64, 2i64], [3i64, 4i64]]);
1585 assert_eq!(line.points[0].components, [1.0, 2.0]);
1586 assert_eq!(line.points[1].components, [3.0, 4.0]);
1587 }
1588
1589 // ---------- MapStyle ----------
1590
1591 fn full_style() -> Style {
1592 Style {
1593 border: Some(egui::Stroke::new(2.0, Color32::RED)),
1594 line: Some(egui::Stroke::new(4.0, Color32::BLUE)),
1595 fill_color: Color32::GREEN,
1596 text_color: Color32::WHITE,
1597 font: Some(FontId::new(10.0, FontFamily::Proportional)),
1598 background_color: Color32::BLACK,
1599 alert_color: Color32::YELLOW,
1600 }
1601 }
1602
1603 #[test]
1604 fn map_style_new() {
1605 let s = Style::new();
1606 assert!(s.border.is_none());
1607 assert!(s.line.is_none());
1608 assert!(s.font.is_none());
1609 assert_eq!(s.fill_color, Color32::TRANSPARENT);
1610 assert_eq!(s.text_color, Color32::TRANSPARENT);
1611 assert_eq!(s.background_color, Color32::TRANSPARENT);
1612 assert_eq!(s.alert_color, Color32::TRANSPARENT);
1613 }
1614
1615 #[test]
1616 fn map_style_default_equals_new() {
1617 let s = Style::default();
1618 assert!(s.border.is_none());
1619 assert!(s.line.is_none());
1620 assert!(s.font.is_none());
1621 }
1622
1623 #[test]
1624 fn map_style_mul_i64() {
1625 let s = full_style() * 2i64;
1626 assert_eq!(s.border.unwrap().width, 4.0);
1627 assert_eq!(s.line.unwrap().width, 8.0);
1628 assert_eq!(s.font.unwrap().size, 20.0);
1629 }
1630
1631 #[test]
1632 fn map_style_mul_i32() {
1633 let s = full_style() * 2i32;
1634 assert_eq!(s.border.unwrap().width, 4.0);
1635 assert_eq!(s.line.unwrap().width, 8.0);
1636 assert_eq!(s.font.unwrap().size, 20.0);
1637 }
1638
1639 #[test]
1640 fn map_style_mul_f32() {
1641 let s = full_style() * 0.5f32;
1642 assert_eq!(s.border.unwrap().width, 1.0);
1643 assert_eq!(s.line.unwrap().width, 2.0);
1644 assert_eq!(s.font.unwrap().size, 5.0);
1645 }
1646
1647 #[test]
1648 fn map_style_mul_f64() {
1649 let s = full_style() * 0.5f64;
1650 assert_eq!(s.border.unwrap().width, 1.0);
1651 assert_eq!(s.line.unwrap().width, 2.0);
1652 assert_eq!(s.font.unwrap().size, 5.0);
1653 }
1654
1655 #[test]
1656 fn map_style_div_i64() {
1657 let s = full_style() / 2i64;
1658 assert_eq!(s.border.unwrap().width, 1.0);
1659 assert_eq!(s.line.unwrap().width, 2.0);
1660 assert_eq!(s.font.unwrap().size, 5.0);
1661 }
1662
1663 #[test]
1664 fn map_style_div_i32() {
1665 let s = full_style() / 2i32;
1666 assert_eq!(s.border.unwrap().width, 1.0);
1667 assert_eq!(s.line.unwrap().width, 2.0);
1668 assert_eq!(s.font.unwrap().size, 5.0);
1669 }
1670
1671 #[test]
1672 fn map_style_div_f32() {
1673 let s = full_style() / 0.5f32;
1674 assert_eq!(s.border.unwrap().width, 4.0);
1675 assert_eq!(s.line.unwrap().width, 8.0);
1676 assert_eq!(s.font.unwrap().size, 20.0);
1677 }
1678
1679 #[test]
1680 fn map_style_div_f64() {
1681 let s = full_style() / 0.5f64;
1682 assert_eq!(s.border.unwrap().width, 4.0);
1683 assert_eq!(s.line.unwrap().width, 8.0);
1684 assert_eq!(s.font.unwrap().size, 20.0);
1685 }
1686
1687 // ---------- MapLabel ----------
1688
1689 #[test]
1690 fn map_label_new() {
1691 let l = MapLabel::new();
1692 assert_eq!(l.text, String::new());
1693 assert_eq!(l.center, Pos2::new(0.0, 0.0));
1694 }
1695
1696 #[test]
1697 fn map_label_default_equals_new() {
1698 let l = MapLabel::default();
1699 assert_eq!(l.text, String::new());
1700 assert_eq!(l.center, Pos2::new(0.0, 0.0));
1701 }
1702
1703 // ---------- MapPoint ----------
1704
1705 #[test]
1706 fn map_point_new() {
1707 let p = MapPoint::new(42, [1.0, 2.0]);
1708 assert_eq!(p.get_id(), 42);
1709 assert_eq!(p.coords, [1.0, 2.0]);
1710 assert!(p.connections.is_empty());
1711 assert_eq!(p.name, None);
1712 assert_eq!(p.get_name(), String::new());
1713 }
1714
1715 #[test]
1716 fn map_point_set_and_get_name() {
1717 let mut p = MapPoint::new(1, [0.0, 0.0]);
1718 p.set_name("Jita".to_string());
1719 assert_eq!(p.name, Some("Jita".to_string()));
1720 assert_eq!(p.get_name(), "Jita");
1721 }
1722
1723 #[test]
1724 fn map_point_from_occupied_entry() {
1725 let mut map: HashMap<usize, MapPoint> = HashMap::new();
1726 let mut original = MapPoint::new(7, [5.0, 6.0]);
1727 original.set_name("Amarr".to_string());
1728 map.insert(7, original);
1729
1730 use std::collections::hash_map::Entry;
1731 if let Entry::Occupied(entry) = map.entry(7) {
1732 let cloned = MapPoint::from(entry);
1733 assert_eq!(cloned.get_id(), 7);
1734 assert_eq!(cloned.get_name(), "Amarr");
1735 assert_eq!(cloned.coords, [5.0, 6.0]);
1736 } else {
1737 panic!("se esperaba una entrada ocupada");
1738 }
1739 }
1740
1741 // ---------- MapBounds ----------
1742
1743 #[test]
1744 fn map_bounds_new() {
1745 let b = MapBounds::new();
1746 assert_eq!(b.min.components, [0.0, 0.0]);
1747 assert_eq!(b.max.components, [0.0, 0.0]);
1748 assert_eq!(b.pos.components, [0.0, 0.0]);
1749 assert_eq!(b.dist, 0.0);
1750 }
1751
1752 #[test]
1753 fn map_bounds_default_equals_new() {
1754 let b = MapBounds::default();
1755 assert_eq!(b.dist, 0.0);
1756 assert_eq!(b.pos.components, [0.0, 0.0]);
1757 }
1758
1759 // ---------- MapSettings ----------
1760
1761 #[test]
1762 fn map_settings_new() {
1763 let s = MapSettings::new();
1764 assert_eq!(s.max_zoom, 0.0);
1765 assert_eq!(s.min_zoom, 0.0);
1766 assert_eq!(s.line_visible_zoom, 0.0);
1767 assert_eq!(s.label_visible_zoom, 0.0);
1768 assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1769 assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1770 assert_eq!(s.node_text_size, 12.0);
1771 assert_eq!(s.label_text_size, 24.0);
1772 assert_eq!(s.styles.len(), 1);
1773 }
1774
1775 #[test]
1776 fn map_settings_default() {
1777 let s = MapSettings::default();
1778 assert_eq!(s.max_zoom, 2.0);
1779 assert_eq!(s.min_zoom, 0.1);
1780 assert_eq!(s.line_visible_zoom, 0.2);
1781 assert_eq!(s.label_visible_zoom, 0.58);
1782 assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1783 assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1784 assert_eq!(s.node_text_size, 12.0);
1785 assert_eq!(s.label_text_size, 24.0);
1786 // light + dark themes
1787 assert_eq!(s.styles.len(), 2);
1788 // light theme
1789 assert_eq!(s.styles[0].background_color, Color32::WHITE);
1790 assert!(s.styles[0].border.is_some());
1791 assert!(s.styles[0].line.is_some());
1792 assert!(s.styles[0].font.is_some());
1793 // dark theme
1794 assert_eq!(s.styles[1].background_color, Color32::DARK_GRAY);
1795 assert!(s.styles[1].border.is_some());
1796 assert!(s.styles[1].line.is_some());
1797 assert!(s.styles[1].font.is_some());
1798 }
1799
1800 // ---------- VisibilitySetting ----------
1801
1802 #[test]
1803 fn visibility_setting_equality() {
1804 assert_eq!(VisibilitySetting::Hidden, VisibilitySetting::Hidden);
1805 assert_eq!(VisibilitySetting::Hover, VisibilitySetting::Hover);
1806 assert_eq!(VisibilitySetting::Always, VisibilitySetting::Always);
1807 assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Hover);
1808 assert_ne!(VisibilitySetting::Hover, VisibilitySetting::Always);
1809 assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Always);
1810 }
1811}