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::Style;
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 background color below is a placeholder, overwritten by
746 // `Map::assign_visual_style` from egui's own visuals on the first
747 // frame. `Style` carries no color of its own -- every color the
748 // widget paints with comes live from the default `MapTheme` (see
749 // `Map::set_theme`/`Map::theme_colors`), so there is nothing here to
750 // keep in sync with a `Theme`.
751
752 // light style
753 obj.styles.push(Style {
754 line_width: Some(2.0),
755 font: Some(FontId::new(12.00, FontFamily::Proportional)),
756 background_color: Color32::WHITE,
757 });
758
759 // dark style
760 obj.styles.push(Style {
761 line_width: Some(2.0),
762 font: Some(FontId::new(12.00, FontFamily::Proportional)),
763 background_color: Color32::DARK_GRAY,
764 });
765 obj
766 }
767}
768
769/// A built-in effect that plays once and ends.
770///
771/// Anchored to the [`Instant`] an event happened, these are the animations
772/// reached through [`NodeHandle`](super::NodeHandle): `map.node(id)?.ripple(t)`.
773/// The widget drops the notification and stops repainting once the effect
774/// finishes. See [`crate::map::animation`] for what each looks like and how
775/// long it runs.
776///
777/// Ignored when a [`NodeTemplate`] is installed — the template's
778/// `notification_ui` takes over. The effects stay reachable there through
779/// [`Animation`](crate::map::animation::Animation).
780#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
781pub enum NodeAnimation {
782 /// Expanding, fading disc. Reads as "one thing happened here".
783 #[default]
784 Pulse,
785 /// Three staggered expanding rings. Reads as "activity is ongoing".
786 Ripple,
787 /// A ring that empties clockwise. Reads as "how old is this information".
788 CountdownArc,
789 /// A disc that overshoots its size and settles. For nodes that just appeared.
790 ScaleIn,
791 /// Four ticks converging on the node. Reads as "target acquired".
792 Crosshair,
793}
794
795/// A built-in effect that runs until it is cleared.
796///
797/// Named after how long it lasts rather than after who uses it, because it has
798/// two consumers: node state set through [`NodeHandle`](super::NodeHandle)
799/// (`map.node(id)?.halo()`), and markers registered with
800/// [`Map::update_marker`](super::Map::update_marker), which pick their look
801/// with [`MapSettings::marker_animation`].
802///
803/// These never end, so the widget keeps requesting repaints for as long as one
804/// is active. That is fine for the handful of elements they are meant for, but
805/// it does keep the app redrawing — see [`crate::map::animation`].
806#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
807pub enum SteadyAnimation {
808 /// Thick ring blinking on and off. The long-standing marker look.
809 #[default]
810 Blink,
811 /// Ring whose opacity breathes in and out. Calmer than [`Self::Blink`].
812 Halo,
813 /// A dot circling the node. Reads as "under observation".
814 Orbit,
815}
816
817/// Which endpoint a [`SegmentAnimation::Comet`] pass starts from.
818///
819/// A segment's own endpoint order (`a`, `b` as loaded through
820/// [`Map::add_lines`](super::Map::add_lines)) is not usually meaningful to a
821/// caller — naming the two ends [`Self::Forward`]/[`Self::Reverse`] instead
822/// keeps the choice about the animation's direction, not about internal
823/// storage order.
824#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
825pub enum CometDirection {
826 /// From the segment's first endpoint to its second.
827 #[default]
828 Forward,
829 /// From the segment's second endpoint to its first.
830 Reverse,
831}
832
833/// A built-in effect that plays once and ends, for a segment.
834///
835/// Anchored to the [`Instant`] an event happened, these are the animations
836/// reached through [`SegmentHandle`](super::SegmentHandle):
837/// `map.segment(id)?.flash(t)`. The widget drops the notification and stops
838/// repainting once the effect finishes. See [`crate::map::animation`] for
839/// what each looks like and how long it runs.
840///
841/// Ignored when a [`SegmentTemplate`] is installed — the template's
842/// `segment_notification_ui` takes over. The effect stays reachable there
843/// through [`Animation`](crate::map::animation::Animation).
844#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
845pub enum SegmentAnimation {
846 /// A brief bright flash on the line that fades back out. The segment
847 /// analogue of [`NodeAnimation::Pulse`] — reads as "something happened on
848 /// this route".
849 #[default]
850 FlashDecay,
851 /// A single dot pass from one endpoint to the other, then gone — the
852 /// event-driven counterpart to [`SteadySegmentAnimation::Comet`]. Reads
853 /// as "one thing moved along this route just now", direction included,
854 /// rather than "traffic keeps flowing this way".
855 Comet(CometDirection),
856 /// The line drawing itself in from the first endpoint to the second,
857 /// then gone. Reads as "this route was just established" rather than
858 /// "something travelled along it".
859 Wipe,
860}
861
862/// A built-in effect that runs until it is cleared, for a segment.
863///
864/// Reached through node state set on [`SegmentHandle`](super::SegmentHandle)
865/// (`map.segment(id)?.comet()` / `.dash()`). Like [`SteadyAnimation`], these
866/// never end, so the widget keeps requesting repaints for as long as one is
867/// active — fine for a handful of highlighted routes, not for every segment
868/// on the map.
869#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
870pub enum SteadySegmentAnimation {
871 /// A dot travelling from one endpoint to the other and looping. Reads as
872 /// "this is the direction of flow".
873 #[default]
874 Comet,
875 /// A dashed line whose pattern slides along the segment ("marching
876 /// ants"). Reads as "route", classic for a path someone might follow.
877 Dash,
878 /// A localized band of brightness travelling the length of the segment
879 /// and looping, fading out before it reaches either end rather than
880 /// snapping back. Reads as "flow", softer and less busy than [`Self::Dash`].
881 GlowBand,
882 /// A row of arrow shapes sliding along the segment, pointing the way.
883 /// Reads as "direction of travel", more explicit than [`Self::Comet`]'s
884 /// single dot.
885 Chevrons,
886}
887
888/// Controls when the name of a node is displayed next to it.
889#[derive(Clone, Debug, PartialEq)]
890pub enum VisibilitySetting {
891 /// Never show node names.
892 Hidden,
893 /// Only show the name of the node closest to the mouse pointer.
894 Hover,
895 /// Always show node names, subject to [`MapSettings::label_visible_zoom`].
896 Always,
897}
898
899/// Provides the contents of the widget's right-click context menu.
900///
901/// Install an implementation with
902/// [`Map::set_context_manager`](super::Map::set_context_manager).
903///
904/// # Examples
905///
906/// ```
907/// use egui_map::map::objects::ContextMenuManager;
908///
909/// struct MyMenu;
910///
911/// impl ContextMenuManager for MyMenu {
912/// fn ui(&self, ui: &mut egui::Ui) {
913/// ui.label("Hello from the map!");
914/// }
915/// }
916/// ```
917pub trait ContextMenuManager {
918 /// Builds the menu contents; called every frame while the menu is open.
919 fn ui(&self, ui: &mut Ui);
920}
921
922/// Customizes how nodes and their visual effects are rendered.
923///
924/// When a template is installed with
925/// [`Map::set_node_template`](super::Map::set_node_template), the widget
926/// delegates all node painting to it instead of using the built-in shapes and
927/// animations — including the node name labels, so draw the name yourself in
928/// [`NodeTemplate::node_ui`] if you need it.
929///
930/// The positions passed to these methods are in screen coordinates: already
931/// scaled by `zoom` and translated to the viewport origin. Multiply every size
932/// by `zoom` so your shapes scale together with the map.
933///
934/// # Animation idioms
935///
936/// egui only repaints on demand, so any method that animates (a blinking
937/// marker, a fading notification, ...) must call
938/// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) to keep the
939/// frames coming. Time-driven effects are usually computed from
940/// [`Instant::now()`] (see `initial_time` in
941/// [`NodeTemplate::notification_ui`]) or from the system clock.
942///
943/// # Examples
944///
945/// A node drawn as a rounded box with its name inside, plus a notification
946/// animation that expands and fades out over two seconds:
947///
948/// ```
949/// use egui_map::map::objects::{MapPoint, NodeContext, NodeTemplate, NotificationContext, MarkerContext, SelectionContext};
950/// use egui::{Align2, Color32, CornerRadius, FontId, Pos2, Rect, Stroke, Ui, Vec2};
951/// use std::time::Instant;
952///
953/// struct BoxedNodes;
954///
955/// impl NodeTemplate for BoxedNodes {
956/// fn node_ui(&self, ui: &mut Ui, ctx: NodeContext) {
957/// // Multiply every size by `ctx.zoom` so the node scales with the map.
958/// let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
959/// let rounding = CornerRadius::same((10.0 * ctx.zoom) as u8);
960/// let painter = ui.painter();
961/// // `ctx.color` is already resolved: `ctx.point.color` if the node has
962/// // its own override, otherwise the active theme's node color.
963/// painter.rect_filled(rect, rounding, ctx.color);
964/// painter.rect_stroke(
965/// rect,
966/// rounding,
967/// Stroke::new(4.0 * ctx.zoom, Color32::WHITE),
968/// egui::StrokeKind::Middle,
969/// );
970/// painter.text(
971/// ctx.position,
972/// Align2::CENTER_CENTER,
973/// ctx.point.get_name(),
974/// FontId::proportional(12.0 * ctx.zoom),
975/// Color32::WHITE,
976/// );
977/// }
978///
979/// fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool {
980/// let secs = Instant::now().duration_since(ctx.initial_time).as_secs_f32();
981/// // Expand the stroke and fade the color out over 2 seconds.
982/// let alpha = (1.0 - secs / 2.0).clamp(0.0, 1.0);
983/// let fading = Color32::from_rgba_unmultiplied(
984/// ctx.color.r(),
985/// ctx.color.g(),
986/// ctx.color.b(),
987/// (255.0 * alpha) as u8,
988/// );
989/// let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
990/// ui.painter().rect_stroke(
991/// rect,
992/// CornerRadius::same((10.0 * ctx.zoom) as u8),
993/// Stroke::new((4.0 + 25.0 * secs) * ctx.zoom, fading),
994/// egui::StrokeKind::Middle,
995/// );
996/// // Keep the animation frames coming.
997/// ui.ctx().request_repaint();
998/// // Returning `false` removes the notification.
999/// secs < 2.0
1000/// }
1001/// # fn selection_ui(&self, ui: &mut Ui, ctx: SelectionContext) {
1002/// # let rect = Rect::from_center_size(ctx.position, Vec2::new(94.0 * ctx.zoom, 39.0 * ctx.zoom));
1003/// # ui.painter().rect_stroke(
1004/// # rect,
1005/// # CornerRadius::same((10.0 * ctx.zoom) as u8),
1006/// # Stroke::new(3.0 * ctx.zoom, ctx.color),
1007/// # egui::StrokeKind::Middle,
1008/// # );
1009/// # }
1010/// # fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext) {
1011/// # ui.painter().circle_stroke(ctx.position, 6.0 * ctx.zoom, Stroke::new(2.0 * ctx.zoom, Color32::LIGHT_GREEN));
1012/// # ui.ctx().request_repaint();
1013/// # }
1014/// }
1015/// ```
1016///
1017/// # Note on `NodeAnimation`/`SteadyAnimation` in the examples above
1018///
1019/// Every method here takes a context struct -- [`NodeContext`],
1020/// [`SelectionContext`], [`NotificationContext`] or [`MarkerContext`] -- each
1021/// `#[non_exhaustive]` so a future field can be added without another
1022/// breaking change to `NodeTemplate` itself.
1023pub trait NodeTemplate {
1024 /// Draws a node, replacing the default filled circle.
1025 ///
1026 /// Called every frame for each visible node. The widget no longer draws
1027 /// the node name once a template is installed, so render it here (e.g.
1028 /// with [`Painter::text`](egui::Painter::text)) if you need it. See
1029 /// [`NodeContext`] for the fields available, in particular `ctx.color`
1030 /// -- the color already resolved for this node, so you don't have to
1031 /// repeat the `point.color.unwrap_or(...)` fallback (or reach for the
1032 /// active theme yourself) to honor a per-node color override.
1033 fn node_ui(&self, ui: &mut Ui, ctx: NodeContext);
1034
1035 /// Draws the highlight over the node closest to the mouse pointer.
1036 ///
1037 /// The nearest node is only computed while the pointer is over the map and
1038 /// [`MapSettings::node_text_visibility`] is [`VisibilitySetting::Hover`].
1039 /// See [`SelectionContext`] for the fields available, in particular
1040 /// `ctx.point` (which node is being highlighted) and `ctx.color` (the
1041 /// active theme's selection color, resolved for you).
1042 fn selection_ui(&self, ui: &mut Ui, ctx: SelectionContext);
1043
1044 /// Draws the notification effect of a node notified at
1045 /// `ctx.initial_time`.
1046 ///
1047 /// Called every frame for each node passed to
1048 /// [`Map::notify`](super::Map::notify) or animated through
1049 /// [`Map::node`](super::Map::node)'s event methods (`pulse`, `ripple`,
1050 /// ...). `ctx.kind` is which of those was requested and `ctx.node_id` is
1051 /// the id of the node it belongs to -- use them to dispatch to the
1052 /// matching built-in [`Animation`](crate::map::animation::Animation)
1053 /// function (or your own effect) instead of reimplementing every
1054 /// animation by hand. See [`NotificationContext`] for the rest of the
1055 /// fields. Should return `true` while the animation is still playing —
1056 /// remember to call
1057 /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) —;
1058 /// once it returns `false` the notification is discarded.
1059 fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool;
1060
1061 /// Draws a marker over the given node.
1062 ///
1063 /// Called every frame for two different things -- see [`MarkerContext`]
1064 /// for what `ctx.kind`/`ctx.node_id` mean in each case. For animated
1065 /// markers (e.g. a blinking light), drive the effect from the system
1066 /// clock and call
1067 /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint).
1068 fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext);
1069}
1070
1071/// The context passed to [`NodeTemplate::node_ui`].
1072///
1073/// `#[non_exhaustive]`, like [`NotificationContext`]/[`MarkerContext`], so a
1074/// future field can be added here without another breaking change.
1075#[derive(Clone, Copy, Debug)]
1076#[non_exhaustive]
1077pub struct NodeContext<'a> {
1078 /// The node's screen position: already scaled by `zoom` and translated
1079 /// to the viewport origin.
1080 pub position: Pos2,
1081 /// Multiply every size you draw by this so it scales with the map.
1082 pub zoom: f32,
1083 /// The node being painted -- its id, name, coordinates and connections.
1084 pub point: &'a MapPoint,
1085 /// The color requested for this node: [`point.color`](MapPoint::color)
1086 /// if the node has its own override, otherwise the active
1087 /// [`MapTheme`](super::theme::MapTheme)'s
1088 /// [`ThemeColors::node`](super::theme::ThemeColors::node) for the
1089 /// current color mode -- the same fallback the built-in circle uses when
1090 /// no template is installed, resolved once here so every `NodeTemplate`
1091 /// doesn't need to repeat it.
1092 pub color: Color32,
1093}
1094
1095/// The context passed to [`NodeTemplate::selection_ui`].
1096///
1097/// `#[non_exhaustive]`, like [`NodeContext`]/[`NotificationContext`]/
1098/// [`MarkerContext`], so a future field can be added here without another
1099/// breaking change.
1100#[derive(Clone, Copy, Debug)]
1101#[non_exhaustive]
1102pub struct SelectionContext<'a> {
1103 /// The node's screen position: already scaled by `zoom` and translated
1104 /// to the viewport origin.
1105 pub position: Pos2,
1106 /// Multiply every size you draw by this so it scales with the map.
1107 pub zoom: f32,
1108 /// The node the highlight belongs to -- the one closest to the mouse
1109 /// pointer. Its id, name, coordinates and connections.
1110 pub point: &'a MapPoint,
1111 /// The active [`MapTheme`](super::theme::MapTheme)'s
1112 /// [`ThemeColors::selected`](super::theme::ThemeColors::selected) for
1113 /// the current color mode -- resolved once here so every `NodeTemplate`
1114 /// doesn't need to reach for the theme itself.
1115 pub color: Color32,
1116}
1117
1118/// The context passed to [`NodeTemplate::notification_ui`].
1119///
1120/// `#[non_exhaustive]` so a future field can be added here without another
1121/// breaking change to [`NodeTemplate`].
1122#[derive(Clone, Copy, Debug)]
1123#[non_exhaustive]
1124pub struct NotificationContext {
1125 /// The node's screen position: already scaled by `zoom` and translated
1126 /// to the viewport origin.
1127 pub position: Pos2,
1128 /// Multiply every size you draw by this so it scales with the map.
1129 pub zoom: f32,
1130 /// When the notification started -- usually fed into a progress
1131 /// computation like `Instant::now().duration_since(initial_time)`.
1132 pub initial_time: Instant,
1133 /// The color requested for this notification (the node's own color, or
1134 /// the current style's `alert_color` if none was set).
1135 pub color: Color32,
1136 /// Which built-in event effect was requested (`pulse`, `ripple`, ...).
1137 /// Match on this to dispatch to the corresponding
1138 /// [`Animation`](crate::map::animation::Animation) function instead of
1139 /// reimplementing the lookup yourself.
1140 pub kind: NodeAnimation,
1141 /// The id of the node this notification belongs to.
1142 pub node_id: usize,
1143}
1144
1145/// The context passed to [`NodeTemplate::marker_ui`].
1146///
1147/// `#[non_exhaustive]`, like [`NotificationContext`], so a future field can
1148/// be added here without another breaking change.
1149#[derive(Clone, Copy, Debug)]
1150#[non_exhaustive]
1151pub struct MarkerContext {
1152 /// The node's screen position: already scaled by `zoom` and translated
1153 /// to the viewport origin.
1154 pub position: Pos2,
1155 /// Multiply every size you draw by this so it scales with the map.
1156 pub zoom: f32,
1157 /// Which persistent effect to draw. For a node's own lasting state (set
1158 /// through [`Map::node`](super::Map::node)'s `halo`/`blink`/`orbit`)
1159 /// this is whichever of those was requested; for a plain marker
1160 /// (registered with [`Map::update_marker`](super::Map::update_marker))
1161 /// it is always [`MapSettings::marker_animation`], since every marker
1162 /// shares that one setting. There is no way from inside this hook to
1163 /// tell the two *cases* apart -- only which `SteadyAnimation` to draw
1164 /// for whichever one it is.
1165 pub kind: SteadyAnimation,
1166 /// The id of the node the state/marker belongs to (for a marker: the id
1167 /// it points at, not the marker's own id).
1168 pub node_id: usize,
1169}
1170
1171/// Customizes how segments and their visual effects are rendered.
1172///
1173/// When a template is installed with
1174/// [`Map::set_segment_template`](super::Map::set_segment_template), the widget
1175/// delegates all segment painting to it instead of using the built-in stroke
1176/// and animations.
1177///
1178/// Unlike [`NodeTemplate`], these methods receive a bare [`&Painter`](Painter)
1179/// rather than `&mut Ui`. Segments are visited in bulk, every frame, after the
1180/// R-tree viewport culling in `paint_map_lines`; going through `Ui` would cost
1181/// a layout pass per segment, on top of what the culling already had to
1182/// discard. Use [`Painter::ctx`] to reach the [`egui::Context`] — for example
1183/// to call `request_repaint()`.
1184///
1185/// The positions passed to these methods are in screen coordinates: already
1186/// scaled by `zoom` and translated to the viewport origin, same as
1187/// [`NodeTemplate`]'s. Multiply every size by `zoom` so your shapes scale
1188/// together with the map.
1189///
1190/// # Examples
1191///
1192/// A segment drawn as a dashed line, plus a notification that briefly
1193/// thickens and brightens it:
1194///
1195/// ```
1196/// use egui_map::map::objects::{MapSegment, SegmentTemplate};
1197/// use egui::{Color32, Painter, Pos2, Stroke};
1198/// use std::time::Instant;
1199///
1200/// struct DashedRoutes;
1201///
1202/// impl SegmentTemplate for DashedRoutes {
1203/// fn segment_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, _segment: &MapSegment) {
1204/// // A crude dash: short strokes along the segment, spaced in screen
1205/// // pixels so they don't stretch as the map zooms.
1206/// let dir = b - a;
1207/// let len = dir.length();
1208/// let step = 10.0 * zoom;
1209/// let mut travelled = 0.0;
1210/// while travelled < len {
1211/// let start = a + dir * (travelled / len);
1212/// let end = a + dir * ((travelled + step * 0.6).min(len) / len);
1213/// painter.line_segment([start, end], Stroke::new(2.0 * zoom, Color32::GRAY));
1214/// travelled += step;
1215/// }
1216/// }
1217///
1218/// fn segment_notification_ui(
1219/// &self,
1220/// painter: &Painter,
1221/// a: Pos2,
1222/// b: Pos2,
1223/// zoom: f32,
1224/// initial_time: Instant,
1225/// color: Color32,
1226/// ) -> bool {
1227/// let secs = Instant::now().duration_since(initial_time).as_secs_f32();
1228/// let alpha = (1.0 - secs).clamp(0.0, 1.0);
1229/// let fading =
1230/// Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), (255.0 * alpha) as u8);
1231/// painter.line_segment([a, b], Stroke::new(5.0 * zoom, fading));
1232/// painter.ctx().request_repaint();
1233/// secs < 1.0
1234/// }
1235///
1236/// fn segment_state_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, time: f32, color: Color32) {
1237/// let t = (time / 1.6).rem_euclid(1.0);
1238/// painter.circle_filled(a + (b - a) * t, 4.0 * zoom, color);
1239/// painter.ctx().request_repaint();
1240/// }
1241/// }
1242/// ```
1243pub trait SegmentTemplate {
1244 /// Draws a segment, replacing the default stroked line.
1245 ///
1246 /// Called every frame for each segment that survives the R-tree viewport
1247 /// culling in `paint_map_lines`.
1248 fn segment_ui(
1249 &self,
1250 painter: &Painter,
1251 pos_a: Pos2,
1252 pos_b: Pos2,
1253 zoom: f32,
1254 segment: &MapSegment,
1255 );
1256
1257 /// Draws the notification effect of a segment notified through
1258 /// [`Map::segment`](super::Map::segment).
1259 ///
1260 /// Called every frame for each segment carrying an event-driven effect
1261 /// (see [`SegmentHandle`](super::SegmentHandle)). Should return `true`
1262 /// while the animation is still playing — remember to call
1263 /// [`Painter::ctx`]`().request_repaint()` — once it returns `false` the
1264 /// notification is discarded.
1265 fn segment_notification_ui(
1266 &self,
1267 painter: &Painter,
1268 pos_a: Pos2,
1269 pos_b: Pos2,
1270 zoom: f32,
1271 initial_time: Instant,
1272 color: Color32,
1273 ) -> bool;
1274
1275 /// Draws the lasting state effect of a segment (e.g. a travelling dot).
1276 ///
1277 /// Called every frame for each segment with lasting state set through
1278 /// [`Map::segment`](super::Map::segment). `time` is the frame time in
1279 /// seconds (`ui.input(|i| i.time)`), so every element animated this frame
1280 /// shares one clock. For animated state, remember to call
1281 /// [`Painter::ctx`]`().request_repaint()`.
1282 fn segment_state_ui(
1283 &self,
1284 painter: &Painter,
1285 pos_a: Pos2,
1286 pos_b: Pos2,
1287 zoom: f32,
1288 time: f32,
1289 color: Color32,
1290 );
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296 use std::collections::HashMap;
1297
1298 // ---------- RawPoint ----------
1299
1300 #[test]
1301 fn raw_point_new() {
1302 let p = RawPoint::new(3.5, -2.0);
1303 assert_eq!(p.components, [3.5, -2.0]);
1304 }
1305
1306 // ---------- MapSegment ----------
1307
1308 #[test]
1309 fn map_segment_new_computes_tight_aabb() {
1310 let seg = MapSegment::new((1, 2), [10.0, -5.0], [-2.0, 7.0]);
1311 assert_eq!(seg.id, (1, 2));
1312 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1313 assert_eq!(envelope.lower(), [-2.0, -5.0]);
1314 assert_eq!(envelope.upper(), [10.0, 7.0]);
1315 assert_eq!(seg.raw_line().points[0].components, [10.0, -5.0]);
1316 assert_eq!(seg.raw_line().points[1].components, [-2.0, 7.0]);
1317 }
1318
1319 #[test]
1320 fn map_segment_envelope_returns_its_aabb() {
1321 let seg = MapSegment::new((1, 2), [0.0, 0.0], [4.0, 2.0]);
1322 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1323 assert_eq!(envelope.lower(), [0.0, 0.0]);
1324 assert_eq!(envelope.upper(), [4.0, 2.0]);
1325 }
1326
1327 #[test]
1328 fn map_segment_degenerate_line_has_point_aabb() {
1329 // A zero-length segment must still produce a valid (empty-area) AABB.
1330 let seg = MapSegment::new((1, 2), [3.0, 3.0], [3.0, 3.0]);
1331 let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1332 assert_eq!(envelope.lower(), [3.0, 3.0]);
1333 assert_eq!(envelope.upper(), [3.0, 3.0]);
1334 }
1335
1336 #[test]
1337 fn raw_point_default() {
1338 let p = RawPoint::default();
1339 assert_eq!(p.components, [0.0, 0.0]);
1340 }
1341
1342 #[test]
1343 fn raw_point_mul_i64() {
1344 let p = RawPoint::new(2.0, -3.0) * 3i64;
1345 assert_eq!(p.components, [6.0, -9.0]);
1346 }
1347
1348 #[test]
1349 fn raw_point_mul_i32() {
1350 let p = RawPoint::new(2.0, -3.0) * 3i32;
1351 assert_eq!(p.components, [6.0, -9.0]);
1352 }
1353
1354 #[test]
1355 fn raw_point_mul_u64() {
1356 let p = RawPoint::new(2.0, -3.0) * 3u64;
1357 assert_eq!(p.components, [6.0, -9.0]);
1358 }
1359
1360 #[test]
1361 fn raw_point_mul_u32() {
1362 let p = RawPoint::new(2.0, -3.0) * 3u32;
1363 assert_eq!(p.components, [6.0, -9.0]);
1364 }
1365
1366 #[test]
1367 fn raw_point_mul_f32() {
1368 let p = RawPoint::new(2.0, -3.0) * 0.5f32;
1369 assert_eq!(p.components, [1.0, -1.5]);
1370 }
1371
1372 #[test]
1373 fn raw_point_mul_assign_i64() {
1374 let mut p = RawPoint::new(2.0, -3.0);
1375 p *= 3i64;
1376 assert_eq!(p.components, [6.0, -9.0]);
1377 }
1378
1379 #[test]
1380 fn raw_point_mul_assign_i32() {
1381 let mut p = RawPoint::new(2.0, -3.0);
1382 p *= 3i32;
1383 assert_eq!(p.components, [6.0, -9.0]);
1384 }
1385
1386 #[test]
1387 fn raw_point_mul_assign_u64() {
1388 let mut p = RawPoint::new(2.0, -3.0);
1389 p *= 3u64;
1390 assert_eq!(p.components, [6.0, -9.0]);
1391 }
1392
1393 #[test]
1394 fn raw_point_mul_assign_u32() {
1395 let mut p = RawPoint::new(2.0, -3.0);
1396 p *= 3u32;
1397 assert_eq!(p.components, [6.0, -9.0]);
1398 }
1399
1400 #[test]
1401 fn raw_point_mul_assign_f32() {
1402 let mut p = RawPoint::new(2.0, -3.0);
1403 p *= 0.5f32;
1404 assert_eq!(p.components, [1.0, -1.5]);
1405 }
1406
1407 #[test]
1408 fn raw_point_div_i64() {
1409 let p = RawPoint::new(6.0, -9.0) / 3i64;
1410 assert_eq!(p.components, [2.0, -3.0]);
1411 }
1412
1413 #[test]
1414 fn raw_point_div_i32() {
1415 let p = RawPoint::new(6.0, -9.0) / 3i32;
1416 assert_eq!(p.components, [2.0, -3.0]);
1417 }
1418
1419 #[test]
1420 fn raw_point_div_u64() {
1421 let p = RawPoint::new(6.0, -9.0) / 3u64;
1422 assert_eq!(p.components, [2.0, -3.0]);
1423 }
1424
1425 #[test]
1426 fn raw_point_div_u32() {
1427 let p = RawPoint::new(6.0, -9.0) / 3u32;
1428 assert_eq!(p.components, [2.0, -3.0]);
1429 }
1430
1431 #[test]
1432 fn raw_point_div_f32() {
1433 let p = RawPoint::new(1.0, -1.5) / 0.5f32;
1434 assert_eq!(p.components, [2.0, -3.0]);
1435 }
1436
1437 #[test]
1438 fn raw_point_div_assign_i64() {
1439 let mut p = RawPoint::new(6.0, -9.0);
1440 p /= 3i64;
1441 assert_eq!(p.components, [2.0, -3.0]);
1442 }
1443
1444 #[test]
1445 fn raw_point_div_assign_i32() {
1446 let mut p = RawPoint::new(6.0, -9.0);
1447 p /= 3i32;
1448 assert_eq!(p.components, [2.0, -3.0]);
1449 }
1450
1451 #[test]
1452 fn raw_point_div_assign_u64() {
1453 let mut p = RawPoint::new(6.0, -9.0);
1454 p /= 3u64;
1455 assert_eq!(p.components, [2.0, -3.0]);
1456 }
1457
1458 #[test]
1459 fn raw_point_div_assign_u32() {
1460 let mut p = RawPoint::new(6.0, -9.0);
1461 p /= 3u32;
1462 assert_eq!(p.components, [2.0, -3.0]);
1463 }
1464
1465 #[test]
1466 fn raw_point_div_assign_f32() {
1467 let mut p = RawPoint::new(1.0, -1.5);
1468 p /= 0.5f32;
1469 assert_eq!(p.components, [2.0, -3.0]);
1470 }
1471
1472 #[test]
1473 fn raw_point_add() {
1474 let a = RawPoint::new(1.0, 2.0);
1475 let b = RawPoint::new(3.0, -4.0);
1476 let c = a + b;
1477 assert_eq!(c.components, [4.0, -2.0]);
1478 }
1479
1480 #[test]
1481 fn raw_point_sub() {
1482 let a = RawPoint::new(1.0, 2.0);
1483 let b = RawPoint::new(3.0, -4.0);
1484 let c = a - b;
1485 assert_eq!(c.components, [-2.0, 6.0]);
1486 }
1487
1488 #[test]
1489 #[allow(clippy::op_ref)] // se prueba a propósito la impl Add<&RawPoint>
1490 fn raw_point_add_ref() {
1491 let a = RawPoint::new(1.0, 2.0);
1492 let b = RawPoint::new(3.0, -4.0);
1493 let c = a + &b;
1494 assert_eq!(c.components, [4.0, -2.0]);
1495 // b sigue siendo usable tras la suma por referencia
1496 assert_eq!(b.components, [3.0, -4.0]);
1497 }
1498
1499 #[test]
1500 #[allow(clippy::op_ref)] // se prueba a propósito la impl Sub<&RawPoint>
1501 fn raw_point_sub_ref() {
1502 let a = RawPoint::new(1.0, 2.0);
1503 let b = RawPoint::new(3.0, -4.0);
1504 let c = a - &b;
1505 assert_eq!(c.components, [-2.0, 6.0]);
1506 assert_eq!(b.components, [3.0, -4.0]);
1507 }
1508
1509 #[test]
1510 fn raw_point_from_f32_array() {
1511 let p = RawPoint::from([1.5f32, -2.5f32]);
1512 assert_eq!(p.components, [1.5, -2.5]);
1513 }
1514
1515 #[test]
1516 fn raw_point_from_i64_array() {
1517 let p = RawPoint::from([3i64, -4i64]);
1518 assert_eq!(p.components, [3.0, -4.0]);
1519 }
1520
1521 #[test]
1522 fn raw_point_from_i32_array() {
1523 let p = RawPoint::from([3i32, -4i32]);
1524 assert_eq!(p.components, [3.0, -4.0]);
1525 }
1526
1527 #[test]
1528 fn raw_point_from_i16_array() {
1529 let p = RawPoint::from([3i16, -4i16]);
1530 assert_eq!(p.components, [3.0, -4.0]);
1531 }
1532
1533 #[test]
1534 fn raw_point_from_i8_array() {
1535 let p = RawPoint::from([3i8, -4i8]);
1536 assert_eq!(p.components, [3.0, -4.0]);
1537 }
1538
1539 #[test]
1540 fn raw_point_from_pos2() {
1541 let p = RawPoint::from(Pos2::new(7.0, 8.0));
1542 assert_eq!(p.components, [7.0, 8.0]);
1543 }
1544
1545 #[test]
1546 fn raw_point_into_f32_array() {
1547 let arr: [f32; 2] = RawPoint::new(7.0, 8.0).into();
1548 assert_eq!(arr, [7.0, 8.0]);
1549 }
1550
1551 #[test]
1552 fn raw_point_into_pos2() {
1553 let pos: Pos2 = RawPoint::new(7.0, 8.0).into();
1554 assert_eq!(pos, Pos2::new(7.0, 8.0));
1555 }
1556
1557 // ---------- RawLine ----------
1558
1559 #[test]
1560 fn raw_line_new() {
1561 let a = RawPoint::new(1.0, 2.0);
1562 let b = RawPoint::new(3.0, 4.0);
1563 let line = RawLine::new(a, b);
1564 assert_eq!(line.points[0].components, [1.0, 2.0]);
1565 assert_eq!(line.points[1].components, [3.0, 4.0]);
1566 }
1567
1568 #[test]
1569 fn raw_line_distance() {
1570 // triángulo 3-4-5
1571 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(3.0, 4.0));
1572 assert_eq!(line.distance(), 5.0);
1573 }
1574
1575 #[test]
1576 fn raw_line_distance_zero() {
1577 let line = RawLine::new(RawPoint::new(2.0, 2.0), RawPoint::new(2.0, 2.0));
1578 assert_eq!(line.distance(), 0.0);
1579 }
1580
1581 #[test]
1582 fn raw_line_midpoint() {
1583 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(4.0, 6.0));
1584 let mid = line.midpoint();
1585 assert_eq!(mid.components, [2.0, 3.0]);
1586 }
1587
1588 #[test]
1589 fn raw_line_distance_to_point_on_segment() {
1590 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1591 assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
1592 assert_eq!(line.distance_to_point(RawPoint::new(5.0, 0.0)), 0.0);
1593 }
1594
1595 #[test]
1596 fn raw_line_distance_to_point_beyond_endpoints() {
1597 let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1598 // Past the end of the segment, the closest point is the endpoint.
1599 assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
1600 assert_eq!(line.distance_to_point(RawPoint::new(-3.0, -4.0)), 5.0);
1601 }
1602
1603 #[test]
1604 fn raw_line_distance_to_point_degenerate_segment() {
1605 let line = RawLine::new(RawPoint::new(1.0, 1.0), RawPoint::new(1.0, 1.0));
1606 assert_eq!(line.distance_to_point(RawPoint::new(4.0, 5.0)), 5.0);
1607 }
1608
1609 #[test]
1610 fn raw_line_into_pos2_array() {
1611 let line = RawLine::new(RawPoint::new(1.0, 2.0), RawPoint::new(3.0, 4.0));
1612 let arr: [Pos2; 2] = line.into();
1613 assert_eq!(arr, [Pos2::new(1.0, 2.0), Pos2::new(3.0, 4.0)]);
1614 }
1615
1616 #[test]
1617 fn raw_line_from_i64_arrays() {
1618 let line = RawLine::from([[1i64, 2i64], [3i64, 4i64]]);
1619 assert_eq!(line.points[0].components, [1.0, 2.0]);
1620 assert_eq!(line.points[1].components, [3.0, 4.0]);
1621 }
1622
1623 // ---------- MapStyle ----------
1624
1625 fn full_style() -> Style {
1626 Style {
1627 line_width: Some(4.0),
1628 font: Some(FontId::new(10.0, FontFamily::Proportional)),
1629 background_color: Color32::BLACK,
1630 }
1631 }
1632
1633 #[test]
1634 fn map_style_new() {
1635 let s = Style::new();
1636 assert!(s.line_width.is_none());
1637 assert!(s.font.is_none());
1638 assert_eq!(s.background_color, Color32::TRANSPARENT);
1639 }
1640
1641 #[test]
1642 fn map_style_default_equals_new() {
1643 let s = Style::default();
1644 assert!(s.line_width.is_none());
1645 assert!(s.font.is_none());
1646 }
1647
1648 #[test]
1649 fn map_style_mul_i64() {
1650 let s = full_style() * 2i64;
1651 assert_eq!(s.line_width.unwrap(), 8.0);
1652 assert_eq!(s.font.unwrap().size, 20.0);
1653 }
1654
1655 #[test]
1656 fn map_style_mul_i32() {
1657 let s = full_style() * 2i32;
1658 assert_eq!(s.line_width.unwrap(), 8.0);
1659 assert_eq!(s.font.unwrap().size, 20.0);
1660 }
1661
1662 #[test]
1663 fn map_style_mul_f32() {
1664 let s = full_style() * 0.5f32;
1665 assert_eq!(s.line_width.unwrap(), 2.0);
1666 assert_eq!(s.font.unwrap().size, 5.0);
1667 }
1668
1669 #[test]
1670 fn map_style_mul_f64() {
1671 let s = full_style() * 0.5f64;
1672 assert_eq!(s.line_width.unwrap(), 2.0);
1673 assert_eq!(s.font.unwrap().size, 5.0);
1674 }
1675
1676 #[test]
1677 fn map_style_div_i64() {
1678 let s = full_style() / 2i64;
1679 assert_eq!(s.line_width.unwrap(), 2.0);
1680 assert_eq!(s.font.unwrap().size, 5.0);
1681 }
1682
1683 #[test]
1684 fn map_style_div_i32() {
1685 let s = full_style() / 2i32;
1686 assert_eq!(s.line_width.unwrap(), 2.0);
1687 assert_eq!(s.font.unwrap().size, 5.0);
1688 }
1689
1690 #[test]
1691 fn map_style_div_f32() {
1692 let s = full_style() / 0.5f32;
1693 assert_eq!(s.line_width.unwrap(), 8.0);
1694 assert_eq!(s.font.unwrap().size, 20.0);
1695 }
1696
1697 #[test]
1698 fn map_style_div_f64() {
1699 let s = full_style() / 0.5f64;
1700 assert_eq!(s.line_width.unwrap(), 8.0);
1701 assert_eq!(s.font.unwrap().size, 20.0);
1702 }
1703
1704 // ---------- MapLabel ----------
1705
1706 #[test]
1707 fn map_label_new() {
1708 let l = MapLabel::new();
1709 assert_eq!(l.text, String::new());
1710 assert_eq!(l.center, Pos2::new(0.0, 0.0));
1711 }
1712
1713 #[test]
1714 fn map_label_default_equals_new() {
1715 let l = MapLabel::default();
1716 assert_eq!(l.text, String::new());
1717 assert_eq!(l.center, Pos2::new(0.0, 0.0));
1718 }
1719
1720 // ---------- MapPoint ----------
1721
1722 #[test]
1723 fn map_point_new() {
1724 let p = MapPoint::new(42, [1.0, 2.0]);
1725 assert_eq!(p.get_id(), 42);
1726 assert_eq!(p.coords, [1.0, 2.0]);
1727 assert!(p.connections.is_empty());
1728 assert_eq!(p.name, None);
1729 assert_eq!(p.get_name(), String::new());
1730 }
1731
1732 #[test]
1733 fn map_point_set_and_get_name() {
1734 let mut p = MapPoint::new(1, [0.0, 0.0]);
1735 p.set_name("Jita".to_string());
1736 assert_eq!(p.name, Some("Jita".to_string()));
1737 assert_eq!(p.get_name(), "Jita");
1738 }
1739
1740 #[test]
1741 fn map_point_from_occupied_entry() {
1742 let mut map: HashMap<usize, MapPoint> = HashMap::new();
1743 let mut original = MapPoint::new(7, [5.0, 6.0]);
1744 original.set_name("Amarr".to_string());
1745 map.insert(7, original);
1746
1747 use std::collections::hash_map::Entry;
1748 if let Entry::Occupied(entry) = map.entry(7) {
1749 let cloned = MapPoint::from(entry);
1750 assert_eq!(cloned.get_id(), 7);
1751 assert_eq!(cloned.get_name(), "Amarr");
1752 assert_eq!(cloned.coords, [5.0, 6.0]);
1753 } else {
1754 panic!("se esperaba una entrada ocupada");
1755 }
1756 }
1757
1758 // ---------- MapBounds ----------
1759
1760 #[test]
1761 fn map_bounds_new() {
1762 let b = MapBounds::new();
1763 assert_eq!(b.min.components, [0.0, 0.0]);
1764 assert_eq!(b.max.components, [0.0, 0.0]);
1765 assert_eq!(b.pos.components, [0.0, 0.0]);
1766 assert_eq!(b.dist, 0.0);
1767 }
1768
1769 #[test]
1770 fn map_bounds_default_equals_new() {
1771 let b = MapBounds::default();
1772 assert_eq!(b.dist, 0.0);
1773 assert_eq!(b.pos.components, [0.0, 0.0]);
1774 }
1775
1776 // ---------- MapSettings ----------
1777
1778 #[test]
1779 fn map_settings_new() {
1780 let s = MapSettings::new();
1781 assert_eq!(s.max_zoom, 0.0);
1782 assert_eq!(s.min_zoom, 0.0);
1783 assert_eq!(s.line_visible_zoom, 0.0);
1784 assert_eq!(s.label_visible_zoom, 0.0);
1785 assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1786 assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1787 assert_eq!(s.node_text_size, 12.0);
1788 assert_eq!(s.label_text_size, 24.0);
1789 assert_eq!(s.styles.len(), 1);
1790 }
1791
1792 #[test]
1793 fn map_settings_default() {
1794 let s = MapSettings::default();
1795 assert_eq!(s.max_zoom, 2.0);
1796 assert_eq!(s.min_zoom, 0.1);
1797 assert_eq!(s.line_visible_zoom, 0.2);
1798 assert_eq!(s.label_visible_zoom, 0.58);
1799 assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1800 assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1801 assert_eq!(s.node_text_size, 12.0);
1802 assert_eq!(s.label_text_size, 24.0);
1803 // light + dark themes
1804 assert_eq!(s.styles.len(), 2);
1805 // light theme
1806 assert_eq!(s.styles[0].background_color, Color32::WHITE);
1807 assert!(s.styles[0].line_width.is_some());
1808 assert!(s.styles[0].font.is_some());
1809 // dark theme
1810 assert_eq!(s.styles[1].background_color, Color32::DARK_GRAY);
1811 assert!(s.styles[1].line_width.is_some());
1812 assert!(s.styles[1].font.is_some());
1813 }
1814
1815 // ---------- VisibilitySetting ----------
1816
1817 #[test]
1818 fn visibility_setting_equality() {
1819 assert_eq!(VisibilitySetting::Hidden, VisibilitySetting::Hidden);
1820 assert_eq!(VisibilitySetting::Hover, VisibilitySetting::Hover);
1821 assert_eq!(VisibilitySetting::Always, VisibilitySetting::Always);
1822 assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Hover);
1823 assert_ne!(VisibilitySetting::Hover, VisibilitySetting::Always);
1824 assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Always);
1825 }
1826}