okf_studio/graph/
layout.rs1use super::model::{EdgeKind, GraphModel, NodeKind};
11use std::collections::HashMap;
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum LayoutMode {
16 #[default]
18 Force,
19 Radial,
21}
22
23impl LayoutMode {
24 #[must_use]
26 pub const fn next(self) -> Self {
27 match self {
28 Self::Force => Self::Radial,
29 Self::Radial => Self::Force,
30 }
31 }
32
33 #[must_use]
35 pub const fn name(self) -> &'static str {
36 match self {
37 Self::Force => "force",
38 Self::Radial => "radial",
39 }
40 }
41}
42
43fn fnv1a(s: &str) -> u64 {
45 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
46 for b in s.as_bytes() {
47 hash ^= u64::from(*b);
48 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
49 }
50 hash
51}
52
53#[derive(Clone, Debug)]
56pub struct LayoutEngine {
57 pub positions: HashMap<String, (f64, f64)>,
59 temperature: f64,
60 pub running: bool,
62}
63
64impl Default for LayoutEngine {
65 fn default() -> Self {
66 Self {
67 positions: HashMap::new(),
68 temperature: 1.0,
69 running: true,
70 }
71 }
72}
73
74impl LayoutEngine {
75 pub fn seed(&mut self, model: &GraphModel) {
79 let missing: Vec<usize> = (0..model.nodes.len())
80 .filter(|&i| !self.positions.contains_key(&model.nodes[i].key))
81 .collect();
82 for &i in &missing {
83 let node = &model.nodes[i];
84 let hash = fnv1a(&node.key);
85 #[allow(clippy::cast_precision_loss)]
86 let angle = (hash % 6283) as f64 / 1000.0;
87 #[allow(clippy::cast_precision_loss)]
88 let radius = 0.35 + ((hash >> 16) % 1000) as f64 / 2000.0;
89 let radius = if node.kind == NodeKind::Phantom {
91 radius + 0.6
92 } else {
93 radius
94 };
95 let mut pos = (radius * angle.cos(), radius * angle.sin());
96 let mut cx = 0.0;
98 let mut cy = 0.0;
99 let mut count = 0;
100 for edge in &model.edges {
101 let other = if edge.from == i {
102 edge.to
103 } else if edge.to == i {
104 edge.from
105 } else {
106 continue;
107 };
108 if let Some(&(x, y)) = self.positions.get(&model.nodes[other].key) {
109 cx += x;
110 cy += y;
111 count += 1;
112 }
113 }
114 if count > 0 {
115 let jitter = 0.05 + f64::from(u32::try_from(hash % 100).unwrap_or(0)) / 1000.0;
116 pos = (
117 cx / f64::from(count) + jitter * angle.cos(),
118 cy / f64::from(count) + jitter * angle.sin(),
119 );
120 }
121 self.positions.insert(node.key.clone(), pos);
122 }
123 if !missing.is_empty() {
124 self.temperature = self.temperature.max(0.3);
125 self.running = true;
126 }
127 }
128
129 #[allow(clippy::cast_precision_loss)]
133 pub fn step(&mut self, model: &GraphModel, included: &[bool], iterations: usize) -> bool {
134 let indices: Vec<usize> = (0..model.nodes.len())
135 .filter(|&i| included.get(i).copied().unwrap_or(false))
136 .collect();
137 let n = indices.len();
138 if n < 2 || !self.running || self.temperature < 0.005 {
139 self.running = false;
140 return false;
141 }
142 let area = 4.0;
143 let k = (area / n as f64).sqrt();
144
145 for _ in 0..iterations {
146 let mut displacements: HashMap<usize, (f64, f64)> = HashMap::new();
147 for (a_pos, &a) in indices.iter().enumerate() {
149 let pa = self.positions[&model.nodes[a].key];
150 for &b in &indices[a_pos + 1..] {
151 let pb = self.positions[&model.nodes[b].key];
152 let (mut dx, mut dy) = (pa.0 - pb.0, pa.1 - pb.1);
153 let mut distance = dx.hypot(dy);
154 if distance < 1e-6 {
155 dx = 1e-3;
157 dy = 1e-3;
158 distance = 1.5e-3;
159 }
160 let hub = 1.0
161 + (model.nodes[a].degree.max(model.nodes[b].degree) as f64).sqrt() / 4.0;
162 let force = k * k / distance * hub;
163 let (ux, uy) = (dx / distance * force, dy / distance * force);
164 let da = displacements.entry(a).or_insert((0.0, 0.0));
165 da.0 += ux;
166 da.1 += uy;
167 let db = displacements.entry(b).or_insert((0.0, 0.0));
168 db.0 -= ux;
169 db.1 -= uy;
170 }
171 }
172 for edge in &model.edges {
174 if !included.get(edge.from).copied().unwrap_or(false)
175 || !included.get(edge.to).copied().unwrap_or(false)
176 {
177 continue;
178 }
179 let pa = self.positions[&model.nodes[edge.from].key];
180 let pb = self.positions[&model.nodes[edge.to].key];
181 let (dx, dy) = (pa.0 - pb.0, pa.1 - pb.1);
182 let distance = dx.hypot(dy).max(1e-6);
183 let ideal = if edge.kind == EdgeKind::Derivation {
184 k * 0.6
185 } else {
186 k
187 };
188 let force = distance * distance / ideal;
189 let (ux, uy) = (dx / distance * force, dy / distance * force);
190 let da = displacements.entry(edge.from).or_insert((0.0, 0.0));
191 da.0 += ux;
192 da.1 += uy;
193 let db = displacements.entry(edge.to).or_insert((0.0, 0.0));
194 db.0 += ux;
195 db.1 += uy;
196 }
197 let limit = self.temperature * 0.1;
199 for &i in &indices {
200 let Some(&(dx, dy)) = displacements.get(&i) else {
201 continue;
202 };
203 let len = dx.hypot(dy);
204 if len < 1e-9 {
205 continue;
206 }
207 let scale = (len.min(limit)) / len;
208 let Some(pos) = self.positions.get_mut(&model.nodes[i].key) else {
209 continue;
210 };
211 pos.0 = dx.mul_add(scale, pos.0);
212 pos.1 = dy.mul_add(scale, pos.1);
213 }
214 self.temperature *= 0.98;
215 }
216 true
217 }
218
219 #[allow(clippy::cast_precision_loss)]
222 pub fn radial(&mut self, model: &GraphModel) {
223 let mut by_depth: HashMap<usize, Vec<usize>> = HashMap::new();
224 for (i, node) in model.nodes.iter().enumerate() {
225 let depth = match node.kind {
226 NodeKind::Phantom | NodeKind::Source => 9,
227 _ => node.id.as_ref().map_or(1, |id| id.segments().len()),
228 };
229 by_depth.entry(depth).or_default().push(i);
230 }
231 let mut depths: Vec<usize> = by_depth.keys().copied().collect();
232 depths.sort_unstable();
233 for (ring, depth) in depths.iter().enumerate() {
234 let members = &by_depth[depth];
235 let radius = (ring as f64).mul_add(0.35, 0.25);
236 let count = members.len().max(1) as f64;
237 let mut ordered = members.clone();
238 ordered.sort_by_key(|&i| model.nodes[i].key.clone());
239 for (slot, &i) in ordered.iter().enumerate() {
240 let angle = std::f64::consts::TAU * slot as f64 / count;
241 self.positions.insert(
242 model.nodes[i].key.clone(),
243 (radius * angle.cos(), radius * angle.sin()),
244 );
245 }
246 }
247 self.running = false;
248 }
249
250 pub const fn toggle_running(&mut self) {
252 self.running = !self.running;
253 if self.running && self.temperature < 0.05 {
254 self.temperature = 0.2;
255 }
256 }
257}