1use serde::{Deserialize, Serialize};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Color {
18 pub r: u8,
19 pub g: u8,
20 pub b: u8,
21 pub a: u8,
22}
23
24impl Color {
25 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
26 Self { r, g, b, a: 255 }
27 }
28 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
29 Self { r, g, b, a }
30 }
31 pub const TRANSPARENT: Color = Color::rgba(0, 0, 0, 0);
32 pub const WHITE: Color = Color::rgb(255, 255, 255);
33 pub const GRAY: Color = Color::rgb(128, 128, 128);
34
35 pub fn dim(self, k: f32) -> Color {
37 Color::rgba(self.r, self.g, self.b, (self.a as f32 * k) as u8)
38 }
39}
40
41impl From<egui::Color32> for Color {
42 fn from(c: egui::Color32) -> Self {
43 let [r, g, b, a] = c.to_srgba_unmultiplied();
44 Color { r, g, b, a }
45 }
46}
47
48impl From<Color> for egui::Color32 {
49 fn from(c: Color) -> Self {
50 egui::Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a)
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
56pub struct Pos {
57 pub x: f32,
58 pub y: f32,
59}
60
61impl Pos {
62 pub const fn new(x: f32, y: f32) -> Self {
63 Self { x, y }
64 }
65}
66
67pub const BOX_W: f32 = 184.0;
70pub const BOX_H: f32 = 34.0;
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75pub struct GraphNode {
76 pub id: String,
77 pub label: String,
78 pub fill: Color,
79 pub stroke: Color,
80 pub pos: Pos,
81}
82
83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
86pub struct GraphEdge {
87 pub from: String,
88 pub to: String,
89 pub color: Color,
90 pub dashed: bool,
91 pub label: Option<String>,
92}
93
94#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
96pub struct GraphModel {
97 pub nodes: Vec<GraphNode>,
98 pub edges: Vec<GraphEdge>,
99}
100
101impl GraphModel {
102 pub fn world_bounds(&self) -> Option<(f32, f32, f32, f32)> {
105 let mut it = self.nodes.iter();
106 let first = it.next()?;
107 let (hw, hh) = (BOX_W * 0.5, BOX_H * 0.5);
108 let mut b = (first.pos.x - hw, first.pos.y - hh, first.pos.x + hw, first.pos.y + hh);
109 for n in it {
110 b.0 = b.0.min(n.pos.x - hw);
111 b.1 = b.1.min(n.pos.y - hh);
112 b.2 = b.2.max(n.pos.x + hw);
113 b.3 = b.3.max(n.pos.y + hh);
114 }
115 Some(b)
116 }
117}
118
119#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
126pub struct NodeDecoration {
127 pub ring: Option<Color>,
128 pub badge: Option<String>,
129 pub badge_color: Option<Color>,
130 pub scale: Option<f32>,
133}
134
135#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
139pub struct Decorations {
140 pub nodes: std::collections::HashMap<String, NodeDecoration>,
142 pub edges: Vec<GraphEdge>,
144}
145
146#[derive(Clone, Copy, Debug)]
150pub struct Camera {
151 pub pan_x: f32,
152 pub pan_y: f32,
153 pub zoom: f32,
154}
155
156impl Default for Camera {
157 fn default() -> Self {
158 Self { pan_x: 0.0, pan_y: 0.0, zoom: 1.0 }
159 }
160}
161
162impl Camera {
163 pub fn fit(model: &GraphModel, w: f32, h: f32, margin: f32) -> Camera {
166 let Some((min_x, min_y, max_x, max_y)) = model.world_bounds() else {
167 return Camera::default();
168 };
169 let (bw, bh) = ((max_x - min_x).max(1.0), (max_y - min_y).max(1.0));
170 let zoom = ((w - 2.0 * margin) / bw).min((h - 2.0 * margin) / bh).clamp(0.05, 4.0);
171 let (cx, cy) = ((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
173 Camera { pan_x: w * 0.5 - cx * zoom, pan_y: h * 0.5 - cy * zoom, zoom }
174 }
175
176 #[inline]
178 pub fn project(&self, p: Pos) -> (f32, f32) {
179 (p.x * self.zoom + self.pan_x, p.y * self.zoom + self.pan_y)
180 }
181}