#[derive(Clone, Debug, PartialEq)]
pub struct NodeGraphMinimap {
pub width: f64,
pub height: f64,
pub zoom: f64,
pub pan: (f64, f64),
pub nodes: Vec<MinimapNode>,
pub connections: Vec<MinimapConnection>,
pub visible: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MinimapNode {
pub id: String,
pub position: (f64, f64),
pub size: (f64, f64),
}
#[derive(Clone, Debug, PartialEq)]
pub struct MinimapConnection {
pub id: String,
pub from: (f64, f64),
pub to: (f64, f64),
}
impl NodeGraphMinimap {
pub fn new(width: f64, height: f64) -> Self {
Self {
width,
height,
zoom: 1.0,
pan: (0.0, 0.0),
nodes: Vec::new(),
connections: Vec::new(),
visible: true,
}
}
pub fn update_view(&mut self, zoom: f64, pan: (f64, f64)) {
self.zoom = zoom;
self.pan = pan;
}
pub fn set_nodes(&mut self, nodes: Vec<MinimapNode>) {
self.nodes = nodes;
}
pub fn set_connections(&mut self, connections: Vec<MinimapConnection>) {
self.connections = connections;
}
pub fn viewport_rect(&self, canvas_width: f64, canvas_height: f64) -> (f64, f64, f64, f64) {
let scale_x = self.width / (canvas_width * self.zoom);
let scale_y = self.height / (canvas_height * self.zoom);
let vp_width = canvas_width * scale_x;
let vp_height = canvas_height * scale_y;
let vp_x = -self.pan.0 * scale_x;
let vp_y = -self.pan.1 * scale_y;
(vp_x, vp_y, vp_width, vp_height)
}
pub fn click_to_pan(
&self,
click_x: f64,
click_y: f64,
canvas_width: f64,
canvas_height: f64,
) -> (f64, f64) {
let total_width = canvas_width * self.zoom;
let total_height = canvas_height * self.zoom;
let new_pan_x = (click_x / self.width) * total_width - total_width / 2.0;
let new_pan_y = (click_y / self.height) * total_height - total_height / 2.0;
(new_pan_x, new_pan_y)
}
pub fn container_style(&self) -> String {
format!(
"position: absolute; bottom: 10px; right: 10px; width: {}px; height: {}px;",
self.width, self.height
)
}
pub fn viewport_style(&self, canvas_width: f64, canvas_height: f64) -> String {
let (x, y, w, h) = self.viewport_rect(canvas_width, canvas_height);
format!(
"position: absolute; left: {}px; top: {}px; width: {}px; height: {}px; border: 1px solid var(--hi-color-primary);",
x, y, w, h
)
}
}
use tairitsu_vdom::svg::SafeSvg;
use tairitsu_vdom::{VElement, VNode};
pub fn render_minimap(minimap: &NodeGraphMinimap) -> VNode {
if !minimap.visible {
return VNode::Element(
VElement::new("div").class("hi-node-graph-minimap hi-minimap-hidden"),
);
}
let canvas_w = 1200.0;
let canvas_h = 800.0;
let scale_x = minimap.width / canvas_w;
let scale_y = minimap.height / canvas_h;
let mut svg_parts = String::new();
svg_parts.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" class="hi-node-graph-minimap" width="{}" height="{}">"#,
minimap.width,
minimap.height,
));
svg_parts.push_str(
r#"<rect width="100%" height="100%" fill="var(--hi-color-minimap-bg, #f8fafc)" rx="4"/>"#,
);
svg_parts.push_str(r#"<g class="hi-minimap-connections">"#);
for conn in &minimap.connections {
svg_parts.push_str(&format!(
r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="var(--hi-color-connection, #94a3b8)" stroke-width="1"/>"#,
conn.from.0 * scale_x,
conn.from.1 * scale_y,
conn.to.0 * scale_x,
conn.to.1 * scale_y,
));
}
svg_parts.push_str("</g>");
svg_parts.push_str(r#"<g class="hi-minimap-nodes">"#);
for node in &minimap.nodes {
svg_parts.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="var(--hi-color-node, #EEA2A4)" rx="2"/>"#,
node.position.0 * scale_x,
node.position.1 * scale_y,
node.size.0 * scale_x,
node.size.1 * scale_y,
));
}
svg_parts.push_str("</g>");
let (vp_x, vp_y, vp_w, vp_h) = minimap.viewport_rect(canvas_w, canvas_h);
svg_parts.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="var(--hi-color-primary, #EEA2A4)" stroke-width="1.5" rx="1"/>"#,
vp_x, vp_y, vp_w, vp_h,
));
svg_parts.push_str("</svg>");
VNode::Element(
VElement::new("div")
.class("hi-node-graph-minimap-container")
.attr("data-action", "minimap-click")
.style(minimap.container_style())
.safe_svg(SafeSvg::new(&svg_parts)),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimap_new() {
let minimap = NodeGraphMinimap::new(200.0, 150.0);
assert_eq!(minimap.width, 200.0);
assert_eq!(minimap.height, 150.0);
assert_eq!(minimap.zoom, 1.0);
assert!(minimap.visible);
}
#[test]
fn test_update_view() {
let mut minimap = NodeGraphMinimap::new(200.0, 150.0);
minimap.update_view(1.5, (10.0, 20.0));
assert_eq!(minimap.zoom, 1.5);
assert_eq!(minimap.pan, (10.0, 20.0));
}
#[test]
fn test_viewport_rect() {
let mut minimap = NodeGraphMinimap::new(200.0, 150.0);
minimap.update_view(1.0, (0.0, 0.0));
let (x, y, w, h) = minimap.viewport_rect(1200.0, 800.0);
assert_eq!(x, 0.0);
assert_eq!(y, 0.0);
assert_eq!(w, 200.0);
assert_eq!(h, 150.0);
}
#[test]
fn test_click_to_pan() {
let minimap = NodeGraphMinimap::new(200.0, 150.0);
let pan = minimap.click_to_pan(100.0, 75.0, 1200.0, 800.0);
assert_eq!(pan, (0.0, 0.0));
}
#[test]
fn test_set_nodes() {
let mut minimap = NodeGraphMinimap::new(200.0, 150.0);
let nodes = vec![MinimapNode {
id: "node1".to_string(),
position: (100.0, 100.0),
size: (200.0, 150.0),
}];
minimap.set_nodes(nodes);
assert_eq!(minimap.nodes.len(), 1);
}
}