Skip to content

Commit

Permalink
Basic fan edges working
Browse files Browse the repository at this point in the history
  • Loading branch information
grtlr committed Dec 2, 2024
1 parent 70bc926 commit e194dc2
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 126 deletions.
22 changes: 11 additions & 11 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "ecolor"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"bytemuck",
"emath",
Expand All @@ -1933,7 +1933,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f"
[[package]]
name = "eframe"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"ahash",
"bytemuck",
Expand Down Expand Up @@ -1972,7 +1972,7 @@ dependencies = [
[[package]]
name = "egui"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"accesskit",
"ahash",
Expand All @@ -1989,7 +1989,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"ahash",
"bytemuck",
Expand All @@ -2008,7 +2008,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"accesskit_winit",
"ahash",
Expand Down Expand Up @@ -2050,7 +2050,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"ahash",
"egui",
Expand All @@ -2067,7 +2067,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"ahash",
"bytemuck",
Expand All @@ -2085,7 +2085,7 @@ dependencies = [
[[package]]
name = "egui_kittest"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"dify",
"egui",
Expand Down Expand Up @@ -2154,7 +2154,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "emath"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"bytemuck",
"serde",
Expand Down Expand Up @@ -2270,7 +2270,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"
dependencies = [
"ab_glyph",
"ahash",
Expand All @@ -2289,7 +2289,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=84cc1572b175d49a64f1b323a6d7e56b1f1fba66#84cc1572b175d49a64f1b323a6d7e56b1f1fba66"
source = "git+https://github.com/emilk/egui.git?rev=6833cf56e17d2581163c43eb36dc3ee5d758771b#6833cf56e17d2581163c43eb36dc3ee5d758771b"

[[package]]
name = "equivalent"
Expand Down
15 changes: 7 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -558,14 +558,13 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg
# As a last resport, patch with a commit to our own repository.
# ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk.

ecolor = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
eframe = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
egui = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
egui_extras = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
emath = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27

egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "84cc1572b175d49a64f1b323a6d7e56b1f1fba66" } # egui master 2024-11-27
ecolor = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
eframe = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
egui = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
egui_extras = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02
emath = { git = "https://github.com/emilk/egui.git", rev = "6833cf56e17d2581163c43eb36dc3ee5d758771b" } # egui master 2024-12-02

# Useful while developing:
# ecolor = { path = "../../egui/crates/ecolor" }
Expand Down
146 changes: 47 additions & 99 deletions crates/viewer/re_space_view_graph/src/layout/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl ForceLayoutProvider {
let target_arrow = edge.target_arrow;
// Self-edges are not supported in force-based layouts.
let anchor =
intersects_ray_from_center(layout.nodes[&edge.source], Vec2::UP);
layout.nodes[&edge.source].intersects_ray_from_center(Vec2::UP);
let geometries = layout
.edges
.entry(EdgeId {
Expand All @@ -155,10 +155,12 @@ impl ForceLayoutProvider {
});
}
}
SlotKind::Regular => {
SlotKind::Regular {
source: slot_source,
target: slot_target,
} => {
if let &[edge] = edges.as_slice() {
// A single regular straight edge.

let target_arrow = edge.target_arrow;
let geometries = layout
.edges
Expand All @@ -175,25 +177,41 @@ impl ForceLayoutProvider {
),
});
} else {
// Multiple edges occupy the same space.
// Multiple edges occupy the same space, so we fan them out.
let num_edges = edges.len();
let fan_amount = 20.0;

for (i, edge) in edges.iter().enumerate() {
// Calculate an offset for the control points based on index `i`
let offset = (i as f32 - (num_edges as f32 / 2.0)) * fan_amount;

let source_rect = layout.nodes[&edge.source];
let target_rect = layout.nodes[&edge.target];
let source_rect = layout.nodes[slot_source];
let target_rect = layout.nodes[slot_target];

let d = (target_rect.center() - source_rect.center()).normalized();

let source_pos = intersects_ray_from_center(source_rect, d);
let target_pos = intersects_ray_from_center(target_rect, -d);
let source_pos = source_rect.intersects_ray_from_center(d);
let target_pos = target_rect.intersects_ray_from_center(-d);

// How far along the edge should the control points be?
let c1_base = source_pos + (target_pos - source_pos) * 0.25;
let c2_base = source_pos + (target_pos - source_pos) * 0.75;

let c1_base_n = Vec2::new(-c1_base.y, c1_base.x).normalized();
let mut c2_base_n = Vec2::new(-c2_base.y, c2_base.x).normalized();

// Compute control points, `c1` and `c2`, based on the offset
let c1 = Pos2::new(source_pos.x + offset, source_pos.y - offset);
let c2 = Pos2::new(target_pos.x + offset, target_pos.y + offset);
// Make sure both point to the same side of the edge.
if c1_base_n.dot(c2_base_n) < 0.0 {
// If they point in opposite directions, flip one of them.
c2_base_n = -c2_base_n;
}

let c1_left = c1_base + c1_base_n * offset;
let c2_left = c2_base + c2_base_n * offset;

// // Compute control points, `c1` and `c2`, based on the offset
// let c1 = Pos2::new(source_pos.x + offset, source_pos.y - offset);
// let c2 = Pos2::new(target_pos.x + offset, target_pos.y + offset);

let geometries = layout
.edges
Expand All @@ -203,13 +221,24 @@ impl ForceLayoutProvider {
})
.or_default();

geometries.push(EdgeGeometry {
target_arrow: edge.target_arrow,
path: PathGeometry::CubicBezier {
// We potentially need to restore the direction of the edge, after we have used it's cannonical form earlier.

Check warning on line 224 in crates/viewer/re_space_view_graph/src/layout/provider.rs

View workflow job for this annotation

GitHub Actions / Checks / Spell Check

"cannonical" should be "canonical".
let path = if edge.source == *slot_source {
PathGeometry::CubicBezier {
source: source_pos,
target: target_pos,
control: [c1, c2],
},
control: [c1_left, c2_left],
}
} else {
PathGeometry::CubicBezier {
source: target_pos,
target: source_pos,
control: [c2_left, c1_left],
}
};

geometries.push(EdgeGeometry {
target_arrow: edge.target_arrow,
path,
});
}
}
Expand All @@ -231,92 +260,11 @@ fn line_segment(source: Rect, target: Rect) -> PathGeometry {
let direction = (target_center - source_center).normalized();

// Find the border points on both rectangles
let source_point = intersects_ray_from_center(source, direction);
let target_point = intersects_ray_from_center(target, -direction); // Reverse direction for target
let source_point = source.intersects_ray_from_center(direction);
let target_point = target.intersects_ray_from_center(-direction); // Reverse direction for target

PathGeometry::Line {
source: source_point,
target: target_point,
}
}

/// Helper function to find the point where the line intersects the border of a rectangle
fn intersects_ray_from_center(rect: Rect, direction: Vec2) -> Pos2 {
let mut tmin = f32::NEG_INFINITY;
let mut tmax = f32::INFINITY;

for i in 0..2 {
let inv_d = 1.0 / -direction[i];
let mut t0 = (rect.min[i] - rect.center()[i]) * inv_d;
let mut t1 = (rect.max[i] - rect.center()[i]) * inv_d;

if inv_d < 0.0 {
std::mem::swap(&mut t0, &mut t1);
}

tmin = tmin.max(t0);
tmax = tmax.min(t1);
}

let t = tmax.min(tmin); // Pick the first intersection
rect.center() + t * -direction
}

#[cfg(test)]
mod test {
use super::*;
use egui::pos2;

#[test]
fn test_ray_intersection() {
let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0));

assert_eq!(
intersects_ray_from_center(rect, Vec2::RIGHT),
pos2(3.0, 2.0),
"rightward ray"
);

assert_eq!(
intersects_ray_from_center(rect, Vec2::UP),
pos2(2.0, 1.0),
"upward ray"
);

assert_eq!(
intersects_ray_from_center(rect, Vec2::LEFT),
pos2(1.0, 2.0),
"leftward ray"
);

assert_eq!(
intersects_ray_from_center(rect, Vec2::DOWN),
pos2(2.0, 3.0),
"downward ray"
);

assert_eq!(
intersects_ray_from_center(rect, (Vec2::LEFT + Vec2::DOWN).normalized()),
pos2(1.0, 3.0),
"bottom-left corner ray"
);

assert_eq!(
intersects_ray_from_center(rect, (Vec2::LEFT + Vec2::UP).normalized()),
pos2(1.0, 1.0),
"top-left corner ray"
);

assert_eq!(
intersects_ray_from_center(rect, (Vec2::RIGHT + Vec2::DOWN).normalized()),
pos2(3.0, 3.0),
"bottom-right corner ray"
);

assert_eq!(
intersects_ray_from_center(rect, (Vec2::RIGHT + Vec2::UP).normalized()),
pos2(3.0, 1.0),
"top-right corner ray"
);
}
}
19 changes: 13 additions & 6 deletions crates/viewer/re_space_view_graph/src/layout/slots.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Force-directed graph layouts assume edges to be straight lines. This
//! function brings edges in a canonical form and finds the ones that occupy the same space.
use crate::graph::{EdgeId, NodeId};
use crate::graph::NodeId;

use super::request::EdgeTemplate;

Expand All @@ -21,8 +21,11 @@ impl SlotId {

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SlotKind {
/// A regular edge going from `source` to `target`.
Regular,
/// An edge slot going from `source` to `target`. Source and target represent the canonical order of the slot, as specified by [`SlotId`]
Regular {
source: NodeId,
target: NodeId,
},
/// An edge where `source == target`.
SelfEdge,
}
Expand All @@ -38,12 +41,16 @@ pub fn slotted_edges<'a>(
let mut slots: ahash::HashMap<SlotId, Slot<'a>> = ahash::HashMap::default();

for e in edges {
let id = SlotId::new(e.source, e.target);
let slot = slots
.entry(SlotId::new(e.source, e.target))
.or_insert_with(|| Slot {
.entry(id)
.or_insert_with_key(|id| Slot {
kind: match e.source == e.target {
true => SlotKind::SelfEdge,
false => SlotKind::Regular,
false => SlotKind::Regular {
source: id.0,
target: id.1,
},
},
edges: Vec::new(),
});
Expand Down
3 changes: 3 additions & 0 deletions crates/viewer/re_space_view_graph/src/ui/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ pub fn draw_edge(ui: &mut Ui, geometry: &EdgeGeometry, show_arrow: bool) -> Resp
fill: Color32::TRANSPARENT,
stroke: stroke.into(),
});

painter.circle(control[0], 1.0, Color32::RED, Stroke::NONE);
painter.circle(control[1], 1.0, Color32::GREEN, Stroke::NONE);
}
}

Expand Down
6 changes: 4 additions & 2 deletions tests/python/release_checklist/check_graph_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
* All graphs have a proper layout.
* The `Weird Graph` views show:
* two self-edges for `A`, a single one for `B`.
* Additionally, there should be three edges between `A` and `B`.
* Additionally, there should be:
* two edges from `A` to `B`.
* one edge from `B` to `A`.
* `graph` has directed edges, while `graph2` has undirected edges.
* `graph` and `graph2` are shown in two different viewers.
* There is a third viewer, `Both`, that shows both `graph` and `graph2` in the same viewer.
Expand All @@ -35,7 +37,7 @@ def log_weird_graph() -> None:
("B", "B"),
# duplicated edges
("A", "B"),
("B", "A"),
("A", "B"),
("B", "A"),
# duplicated self-edges
("A", "A"),
Expand Down

0 comments on commit e194dc2

Please sign in to comment.