1use std::f64::consts::PI;
8
9use crate::core::{Graph, IgraphResult, rng::SplitMix64};
10
11pub fn layout_random(graph: &Graph, seed: u64) -> Vec<(f64, f64)> {
29 let n = graph.vcount() as usize;
30 let mut rng = SplitMix64::new(seed);
31 (0..n)
32 .map(|_| {
33 let x = 2.0 * rng.gen_unit() - 1.0;
34 let y = 2.0 * rng.gen_unit() - 1.0;
35 (x, y)
36 })
37 .collect()
38}
39
40pub fn layout_random_3d(graph: &Graph, seed: u64) -> Vec<(f64, f64, f64)> {
54 let n = graph.vcount() as usize;
55 let mut rng = SplitMix64::new(seed);
56 (0..n)
57 .map(|_| {
58 let x = 2.0 * rng.gen_unit() - 1.0;
59 let y = 2.0 * rng.gen_unit() - 1.0;
60 let z = 2.0 * rng.gen_unit() - 1.0;
61 (x, y, z)
62 })
63 .collect()
64}
65
66#[allow(clippy::cast_precision_loss)]
84pub fn layout_circle(graph: &Graph, order: Option<&[u32]>) -> Vec<(f64, f64)> {
85 let n = graph.vcount() as usize;
86 let mut coords = vec![(0.0, 0.0); n];
87
88 match order {
89 Some(ord) => {
90 let vs_size = ord.len();
91 for (i, &v) in ord.iter().enumerate() {
92 let v_us = v as usize;
93 if v_us < n {
94 let phi = 2.0 * PI / (vs_size as f64) * (i as f64);
95 coords[v_us] = (phi.cos(), phi.sin());
96 }
97 }
98 }
99 None => {
100 for (i, slot) in coords.iter_mut().enumerate() {
101 let phi = 2.0 * PI / (n as f64) * (i as f64);
102 *slot = (phi.cos(), phi.sin());
103 }
104 }
105 }
106
107 coords
108}
109
110#[allow(clippy::cast_precision_loss)]
132pub fn layout_star(
133 graph: &Graph,
134 center: u32,
135 order: Option<&[u32]>,
136) -> IgraphResult<Vec<(f64, f64)>> {
137 let n = graph.vcount();
138 let n_us = n as usize;
139
140 if n > 0 && center >= n {
141 return Err(crate::core::IgraphError::InvalidArgument(
142 "center vertex out of range".to_string(),
143 ));
144 }
145 if let Some(ord) = order {
146 if ord.len() != n_us {
147 return Err(crate::core::IgraphError::InvalidArgument(
148 "order vector length must equal vertex count".to_string(),
149 ));
150 }
151 }
152
153 let mut coords = vec![(0.0, 0.0); n_us];
154
155 if n_us <= 1 {
156 return Ok(coords);
157 }
158
159 let ring_count = n_us - 1;
160 let mut phi = 0.0_f64;
161
162 for i in 0..n_us {
163 #[allow(clippy::cast_possible_truncation)]
164 let node = order.map_or(i as u32, |ord| ord[i]);
165 if node != center {
166 coords[node as usize] = (phi.cos(), phi.sin());
167 phi += 2.0 * PI / (ring_count as f64);
168 }
169 }
170
171 Ok(coords)
172}
173
174#[allow(
195 clippy::cast_precision_loss,
196 clippy::cast_possible_truncation,
197 clippy::cast_sign_loss
198)]
199pub fn layout_grid(graph: &Graph, width: i32) -> Vec<(f64, f64)> {
200 let n = graph.vcount() as usize;
201
202 let w = if width <= 0 {
203 (n as f64).sqrt().ceil() as usize
204 } else {
205 width as usize
206 };
207
208 let mut coords = Vec::with_capacity(n);
209 let mut x = 0usize;
210 let mut y = 0usize;
211
212 for _ in 0..n {
213 coords.push((x as f64, y as f64));
214 x += 1;
215 if x == w {
216 x = 0;
217 y += 1;
218 }
219 }
220
221 coords
222}
223
224#[allow(
238 clippy::cast_precision_loss,
239 clippy::cast_possible_truncation,
240 clippy::cast_sign_loss,
241 clippy::many_single_char_names
242)]
243pub fn layout_grid_3d(graph: &Graph, width: i32, height: i32) -> Vec<(f64, f64, f64)> {
244 let n = graph.vcount() as usize;
245
246 let (w, h) = match (width <= 0, height <= 0) {
247 (true, true) => {
248 let side = (n as f64).cbrt().ceil() as usize;
249 (side.max(1), side.max(1))
250 }
251 (true, false) => {
252 let ht = height as usize;
253 let wd = ((n as f64) / (ht as f64)).sqrt().ceil() as usize;
254 (wd.max(1), ht)
255 }
256 (false, true) => {
257 let wd = width as usize;
258 let ht = ((n as f64) / (wd as f64)).sqrt().ceil() as usize;
259 (wd, ht.max(1))
260 }
261 (false, false) => (width as usize, height as usize),
262 };
263
264 let mut coords = Vec::with_capacity(n);
265 let mut x = 0usize;
266 let mut y = 0usize;
267 let mut z = 0usize;
268
269 for _ in 0..n {
270 coords.push((x as f64, y as f64, z as f64));
271 x += 1;
272 if x == w {
273 x = 0;
274 y += 1;
275 if y == h {
276 y = 0;
277 z += 1;
278 }
279 }
280 }
281
282 coords
283}
284
285#[allow(clippy::cast_precision_loss)]
305pub fn layout_sphere(graph: &Graph) -> Vec<(f64, f64, f64)> {
306 let n = graph.vcount() as usize;
307
308 if n == 0 {
309 return Vec::new();
310 }
311 if n == 1 {
312 return vec![(0.0, 0.0, -1.0)];
313 }
314
315 let sqrt_n = (n as f64).sqrt();
316 let mut coords = Vec::with_capacity(n);
317 let mut phi = 0.0_f64;
318
319 for i in 0..n {
320 let (z, r) = if i == 0 {
321 (-1.0, 0.0)
322 } else if i == n - 1 {
323 (1.0, 0.0)
324 } else {
325 let z_val = -1.0 + 2.0 * (i as f64) / ((n - 1) as f64);
326 let r_val = (1.0 - z_val * z_val).sqrt();
327 phi += 3.6 / (sqrt_n * r_val);
328 (z_val, r_val)
329 };
330
331 coords.push((r * phi.cos(), r * phi.sin(), z));
332 }
333
334 coords
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::create;
341
342 fn approx_eq(a: f64, b: f64) -> bool {
343 (a - b).abs() < 1e-9
344 }
345
346 #[test]
347 fn random_empty() {
348 let g = Graph::with_vertices(0);
349 let c = layout_random(&g, 0);
350 assert!(c.is_empty());
351 }
352
353 #[test]
354 fn random_bounds() {
355 let g = Graph::with_vertices(100);
356 let c = layout_random(&g, 42);
357 assert_eq!(c.len(), 100);
358 for &(x, y) in &c {
359 assert!((-1.0..=1.0).contains(&x), "x={x} out of bounds");
360 assert!((-1.0..=1.0).contains(&y), "y={y} out of bounds");
361 }
362 }
363
364 #[test]
365 fn random_deterministic() {
366 let g = Graph::with_vertices(10);
367 let a = layout_random(&g, 123);
368 let b = layout_random(&g, 123);
369 assert_eq!(a, b);
370 }
371
372 #[test]
373 fn random_3d_bounds() {
374 let g = Graph::with_vertices(50);
375 let c = layout_random_3d(&g, 42);
376 assert_eq!(c.len(), 50);
377 for &(x, y, z) in &c {
378 assert!((-1.0..=1.0).contains(&x));
379 assert!((-1.0..=1.0).contains(&y));
380 assert!((-1.0..=1.0).contains(&z));
381 }
382 }
383
384 #[test]
385 fn circle_empty() {
386 let g = Graph::with_vertices(0);
387 let c = layout_circle(&g, None);
388 assert!(c.is_empty());
389 }
390
391 #[test]
392 fn circle_single() {
393 let g = Graph::with_vertices(1);
394 let c = layout_circle(&g, None);
395 assert_eq!(c.len(), 1);
396 assert!(approx_eq(c[0].0, 1.0));
397 assert!(approx_eq(c[0].1, 0.0));
398 }
399
400 #[test]
401 fn circle_4_vertices() {
402 let g = Graph::with_vertices(4);
403 let c = layout_circle(&g, None);
404 assert_eq!(c.len(), 4);
405 assert!(approx_eq(c[0].0, 1.0));
406 assert!(approx_eq(c[0].1, 0.0));
407 assert!(approx_eq(c[1].0, 0.0));
408 assert!(approx_eq(c[1].1, 1.0));
409 assert!(approx_eq(c[2].0, -1.0));
410 assert!(approx_eq(c[2].1, 0.0));
411 assert!(approx_eq(c[3].0, 0.0));
412 assert!(approx_eq(c[3].1, -1.0));
413 }
414
415 #[test]
416 fn circle_with_order() {
417 let g = Graph::with_vertices(3);
418 let c = layout_circle(&g, Some(&[2, 0, 1]));
419 assert!(approx_eq(c[2].0, 1.0));
420 assert!(approx_eq(c[2].1, 0.0));
421 }
422
423 #[test]
424 fn circle_unit_radius() {
425 let g = Graph::with_vertices(12);
426 let c = layout_circle(&g, None);
427 for (i, &(x, y)) in c.iter().enumerate() {
428 let r = (x * x + y * y).sqrt();
429 assert!(approx_eq(r, 1.0), "v{i}: r={r}");
430 }
431 }
432
433 #[test]
434 fn star_empty() {
435 let g = Graph::with_vertices(0);
436 let c = layout_star(&g, 0, None).expect("ok");
437 assert!(c.is_empty());
438 }
439
440 #[test]
441 fn star_single() {
442 let g = Graph::with_vertices(1);
443 let c = layout_star(&g, 0, None).expect("ok");
444 assert_eq!(c.len(), 1);
445 assert!(approx_eq(c[0].0, 0.0));
446 assert!(approx_eq(c[0].1, 0.0));
447 }
448
449 #[test]
450 fn star_center_at_origin() {
451 let g = Graph::with_vertices(5);
452 let c = layout_star(&g, 2, None).expect("ok");
453 assert!(approx_eq(c[2].0, 0.0));
454 assert!(approx_eq(c[2].1, 0.0));
455 }
456
457 #[test]
458 fn star_ring_unit_radius() {
459 let g = Graph::with_vertices(5);
460 let c = layout_star(&g, 0, None).expect("ok");
461 for (i, &(x, y)) in c.iter().enumerate().skip(1) {
462 let r = (x * x + y * y).sqrt();
463 assert!(approx_eq(r, 1.0), "v{i}: r={r}");
464 }
465 }
466
467 #[test]
468 fn star_invalid_center() {
469 let g = Graph::with_vertices(3);
470 let r = layout_star(&g, 5, None);
471 assert!(r.is_err());
472 }
473
474 #[test]
475 fn star_invalid_order_len() {
476 let g = Graph::with_vertices(3);
477 let r = layout_star(&g, 0, Some(&[0, 1]));
478 assert!(r.is_err());
479 }
480
481 #[test]
482 fn grid_empty() {
483 let g = Graph::with_vertices(0);
484 let c = layout_grid(&g, 3);
485 assert!(c.is_empty());
486 }
487
488 #[test]
489 fn grid_2x3() {
490 let g = Graph::with_vertices(6);
491 let c = layout_grid(&g, 3);
492 assert_eq!(c[0], (0.0, 0.0));
493 assert_eq!(c[1], (1.0, 0.0));
494 assert_eq!(c[2], (2.0, 0.0));
495 assert_eq!(c[3], (0.0, 1.0));
496 assert_eq!(c[4], (1.0, 1.0));
497 assert_eq!(c[5], (2.0, 1.0));
498 }
499
500 #[test]
501 fn grid_auto_width() {
502 let g = Graph::with_vertices(9);
503 let c = layout_grid(&g, 0);
504 assert_eq!(c.len(), 9);
505 assert_eq!(c[3], (0.0, 1.0));
506 }
507
508 #[test]
509 fn grid_3d_basic() {
510 let g = Graph::with_vertices(8);
511 let c = layout_grid_3d(&g, 2, 2);
512 assert_eq!(c.len(), 8);
513 assert_eq!(c[0], (0.0, 0.0, 0.0));
514 assert_eq!(c[1], (1.0, 0.0, 0.0));
515 assert_eq!(c[2], (0.0, 1.0, 0.0));
516 assert_eq!(c[3], (1.0, 1.0, 0.0));
517 assert_eq!(c[4], (0.0, 0.0, 1.0));
518 }
519
520 #[test]
521 fn grid_3d_auto() {
522 let g = Graph::with_vertices(27);
523 let c = layout_grid_3d(&g, 0, 0);
524 assert_eq!(c.len(), 27);
525 }
526
527 #[test]
528 fn sphere_empty() {
529 let g = Graph::with_vertices(0);
530 let c = layout_sphere(&g);
531 assert!(c.is_empty());
532 }
533
534 #[test]
535 fn sphere_single() {
536 let g = Graph::with_vertices(1);
537 let c = layout_sphere(&g);
538 assert_eq!(c.len(), 1);
539 assert!(approx_eq(c[0].2, -1.0));
540 }
541
542 #[test]
543 fn sphere_two() {
544 let g = Graph::with_vertices(2);
545 let c = layout_sphere(&g);
546 assert!(approx_eq(c[0].2, -1.0));
547 assert!(approx_eq(c[1].2, 1.0));
548 }
549
550 #[test]
551 fn sphere_unit_radius() {
552 let g = Graph::with_vertices(50);
553 let c = layout_sphere(&g);
554 for (i, &(x, y, z)) in c.iter().enumerate() {
555 let r = (x * x + y * y + z * z).sqrt();
556 assert!((r - 1.0).abs() < 0.01, "v{i}: r={r}");
557 }
558 }
559
560 #[test]
561 fn sphere_poles() {
562 let g = Graph::with_vertices(10);
563 let c = layout_sphere(&g);
564 assert!(approx_eq(c[0].2, -1.0));
565 assert!(approx_eq(c[9].2, 1.0));
566 }
567
568 #[test]
569 fn layout_ignores_edges() {
570 let g = create(&[(0, 1), (1, 2), (2, 0)], 3, false).expect("ok");
571 let a = layout_circle(&g, None);
572 let b = layout_circle(&Graph::with_vertices(3), None);
573 assert_eq!(a, b);
574 }
575}
576
577#[cfg(all(test, feature = "proptest-harness"))]
578mod proptests {
579 use super::*;
580 use proptest::prelude::*;
581
582 proptest! {
583 #[test]
584 fn random_all_in_bounds(n in 0u32..100, seed in 0u64..10000) {
585 let g = Graph::with_vertices(n);
586 let c = layout_random(&g, seed);
587 prop_assert_eq!(c.len(), n as usize);
588 for (i, &(x, y)) in c.iter().enumerate() {
589 prop_assert!(
590 (-1.0..=1.0).contains(&x),
591 "v{i}: x={x} out of [-1,1]"
592 );
593 prop_assert!(
594 (-1.0..=1.0).contains(&y),
595 "v{i}: y={y} out of [-1,1]"
596 );
597 }
598 }
599
600 #[test]
601 fn circle_all_on_unit_circle(n in 1u32..50) {
602 let g = Graph::with_vertices(n);
603 let c = layout_circle(&g, None);
604 for (i, &(x, y)) in c.iter().enumerate() {
605 let r = (x * x + y * y).sqrt();
606 prop_assert!(
607 (r - 1.0).abs() < 1e-9,
608 "v{i}: r={r} not on unit circle"
609 );
610 }
611 }
612
613 #[test]
614 fn grid_covers_expected_positions(n in 1u32..50) {
615 let g = Graph::with_vertices(n);
616 let c = layout_grid(&g, 0);
617 prop_assert_eq!(c.len(), n as usize);
618 for &(x, y) in &c {
619 prop_assert!(x >= 0.0);
620 prop_assert!(y >= 0.0);
621 }
622 }
623
624 #[test]
625 fn sphere_near_unit(n in 2u32..100) {
626 let g = Graph::with_vertices(n);
627 let c = layout_sphere(&g);
628 for (i, &(x, y, z)) in c.iter().enumerate() {
629 let r = (x * x + y * y + z * z).sqrt();
630 prop_assert!(
631 (r - 1.0).abs() < 0.02,
632 "v{i}: r={r} not near unit sphere"
633 );
634 }
635 }
636 }
637}