use bevy::prelude::*;
use bevy_egui::egui;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarViewport { pub pan: (f32, f32), pub zoom: f32 }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NodeLayout { pub pos: (f32, f32), pub collapsed: bool }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EdgeLayout { pub pill_center: (f32, f32) }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Sidecar {
pub schema_version: u32,
pub scene_basename: Option<String>,
pub scene_hash: Option<String>,
pub graph_fingerprint: Option<String>,
pub viewport: Option<SidecarViewport>,
pub nodes: std::collections::HashMap<String, NodeLayout>,
pub edges: std::collections::HashMap<String, EdgeLayout>,
}
impl Sidecar {
pub fn new() -> Self {
Self { schema_version: 1, scene_basename: None, scene_hash: None, graph_fingerprint: None, viewport: None, nodes: Default::default(), edges: Default::default() }
}
}
pub fn load_sidecar(path: impl AsRef<std::path::Path>) -> std::io::Result<Sidecar> {
let text = std::fs::read_to_string(path)?;
let sidecar: Sidecar = ron::de::from_str(&text).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("ron: {e}")))?;
Ok(sidecar)
}
pub fn parse_sidecar_text(text: &str) -> std::io::Result<Sidecar> {
let sidecar: Sidecar = ron::de::from_str(text).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("ron: {e}")))?;
Ok(sidecar)
}
use crate::editor::view_model::GraphDoc;
use crate::model::StateMachineGraph;
use crate::types::EntityId;
fn get_node_name(graph: &StateMachineGraph, id: &EntityId) -> String { graph.get_display_name(id) }
fn build_parent_path(graph: &StateMachineGraph, id: &EntityId) -> String {
let mut parts: Vec<String> = Vec::new();
let mut cur = Some(*id);
while let Some(cid) = cur { parts.push(get_node_name(graph, &cid)); cur = graph.get_parent(&cid); }
parts.reverse();
parts.join("/")
}
pub fn node_key(graph: &StateMachineGraph, id: &EntityId) -> String {
let parent = graph.get_parent(id);
let parent_path = match parent { Some(pid) => build_parent_path(graph, &pid), None => String::new() };
let name = get_node_name(graph, id);
format!("{}|{}", parent_path, name)
}
fn legacy_build_parent_path(graph: &StateMachineGraph, id: &EntityId) -> String {
let mut parts: Vec<String> = Vec::new();
let mut cur = Some(*id);
while let Some(cid) = cur { let p = graph.get_parent(&cid); if p.is_some() { cur = p; parts.push(get_node_name(graph, &cid)); continue; } else { parts.push(get_node_name(graph, &cid)); break; } }
if parts.len() >= 1 { let _ = parts.remove(0); }
parts.reverse();
parts.join("/")
}
fn legacy_node_key(graph: &StateMachineGraph, id: &EntityId) -> String {
let parent = graph.get_parent(id);
let parent_path = match parent { Some(pid) => legacy_build_parent_path(graph, &pid), None => String::new() };
let name = get_node_name(graph, id);
format!("{}|{}", parent_path, name)
}
pub fn edge_key(graph: &StateMachineGraph, eid: &EntityId) -> String {
let e = match graph.edges.get(eid) { Some(e) => e, None => return format!("{:?}", eid) };
let src = node_key(graph, &e.source);
let dst = node_key(graph, &e.target);
let label = e.display_label.clone().unwrap_or_else(|| "Edge".to_string());
format!("{} -> {}|{}", src, dst, label)
}
fn legacy_edge_key(graph: &StateMachineGraph, eid: &EntityId) -> String {
let e = match graph.edges.get(eid) { Some(e) => e, None => return format!("{:?}", eid) };
let src = legacy_node_key(graph, &e.source);
let dst = legacy_node_key(graph, &e.target);
let label = e.display_label.clone().unwrap_or_else(|| "Edge".to_string());
format!("{} -> {}|{}", src, dst, label)
}
pub fn compute_graph_fingerprint(graph: &StateMachineGraph) -> String {
let mut node_keys: Vec<String> = graph.nodes.keys().map(|id| node_key(graph, id)).collect();
node_keys.sort();
let mut edge_keys: Vec<String> = graph.edges.keys().map(|id| edge_key(graph, id)).collect();
edge_keys.sort();
let canonical = format!("nodes:\n{}\nedges:\n{}", node_keys.join("\n"), edge_keys.join("\n"));
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let bytes = hasher.finalize();
format!("sha256:{}", hex::encode(bytes))
}
pub fn extract_sidecar_for_subtree(doc: &GraphDoc, root: &EntityId) -> Sidecar {
let mut sc = Sidecar::new();
if let Some(graph) = &doc.graph {
let base_min = doc.scene.node_rects.get(root).map(|r| r.min).unwrap_or(egui::pos2(0.0, 0.0));
use std::collections::{HashSet, VecDeque};
let mut nodes_set: HashSet<EntityId> = HashSet::new();
let mut q: VecDeque<EntityId> = VecDeque::new();
q.push_back(*root);
while let Some(cur) = q.pop_front() { if !nodes_set.insert(cur) { continue; } for child in graph.get_children(&cur).into_iter() { q.push_back(child); } }
let mut sub = crate::model::StateMachineGraph::new(crate::model::StateNode::new(*root));
for id in nodes_set.iter() {
if let Some(n) = graph.nodes.get(id) { sub.nodes.insert(*id, n.clone()); }
}
for (eid, e) in graph.edges.iter() {
if nodes_set.contains(&e.source) && nodes_set.contains(&e.target) {
sub.edges.insert(*eid, e.clone());
}
}
sc.graph_fingerprint = Some(compute_graph_fingerprint(&sub));
for (id, sv) in doc.scene.states.iter() {
if !nodes_set.contains(id) { continue; }
let key = node_key(&sub, id);
sc.nodes.insert(key, NodeLayout { pos: (sv.rect.min.x - base_min.x, sv.rect.min.y - base_min.y), collapsed: false });
}
for (eid, _ev) in doc.scene.edges.iter() {
if !sub.edges.contains_key(eid) { continue; }
let key = edge_key(&sub, eid);
let center = doc.scene.node_rects.get(eid).map(|r| r.center()).unwrap_or(egui::pos2(0.0, 0.0));
sc.edges.insert(key, EdgeLayout { pill_center: (center.x - base_min.x, center.y - base_min.y) });
}
}
sc.viewport = Some(SidecarViewport { pan: (doc.transform.pan.x, doc.transform.pan.y), zoom: doc.transform.zoom });
sc
}
pub fn apply_sidecar_to_doc(doc: &mut GraphDoc, sidecar: &Sidecar) {
if let Some(graph) = &doc.graph {
for (id, sv) in doc.scene.states.iter_mut() {
let key = node_key(graph, id);
let mut found = sidecar.nodes.get(&key);
if found.is_none() {
let legacy = legacy_node_key(graph, id);
found = sidecar.nodes.get(&legacy);
}
if let Some(n) = found {
let size = sv.rect.size();
sv.rect = egui::Rect::from_min_size(egui::pos2(n.pos.0, n.pos.1), size);
if let Some(r) = doc.scene.node_rects.get_mut(id) { *r = sv.rect; }
}
}
for (eid, ev) in doc.scene.edges.iter_mut() {
let key = edge_key(graph, eid);
let mut found = sidecar.edges.get(&key);
if found.is_none() {
let legacy = legacy_edge_key(graph, eid);
found = sidecar.edges.get(&legacy);
}
if let Some(e) = found {
let size = ev.rect.size();
let min = egui::pos2(e.pill_center.0 - size.x * 0.5, e.pill_center.1 - size.y * 0.5);
ev.rect = egui::Rect::from_min_size(min, size);
if let Some(r) = doc.scene.node_rects.get_mut(eid) { *r = ev.rect; }
}
}
}
}
pub fn apply_sidecar_to_subtree(doc: &mut GraphDoc, sidecar: &Sidecar, root: &EntityId) {
use std::collections::{HashSet, VecDeque};
if doc.graph.is_none() { return; }
let graph = doc.graph.clone().unwrap();
let base_min = doc.scene.node_rects.get(root).map(|r| r.min).unwrap_or(egui::pos2(0.0, 0.0));
let mut nodes_set: HashSet<EntityId> = HashSet::new();
let mut q: VecDeque<EntityId> = VecDeque::new();
q.push_back(*root);
while let Some(cur) = q.pop_front() { if !nodes_set.insert(cur) { continue; } for child in graph.get_children(&cur).into_iter() { q.push_back(child); } }
let mut sub = crate::model::StateMachineGraph::new(crate::model::StateNode::new(*root));
for id in nodes_set.iter() {
if let Some(n) = graph.nodes.get(id) { sub.nodes.insert(*id, n.clone()); }
}
for (eid, e) in graph.edges.iter() {
if nodes_set.contains(&e.source) && nodes_set.contains(&e.target) { sub.edges.insert(*eid, e.clone()); }
}
for (id, sv) in doc.scene.states.iter_mut() {
if !nodes_set.contains(id) { continue; }
let key = node_key(&sub, id);
let mut found = sidecar.nodes.get(&key);
if found.is_none() {
let legacy = legacy_node_key(&sub, id);
found = sidecar.nodes.get(&legacy);
}
if let Some(n) = found {
let size = sv.rect.size();
sv.rect = egui::Rect::from_min_size(egui::pos2(n.pos.0 + base_min.x, n.pos.1 + base_min.y), size);
if let Some(r) = doc.scene.node_rects.get_mut(id) { *r = sv.rect; }
}
}
for (eid, ev) in doc.scene.edges.iter_mut() {
if !sub.edges.contains_key(eid) { continue; }
let key = edge_key(&sub, eid);
let mut found = sidecar.edges.get(&key);
if found.is_none() {
let legacy = legacy_edge_key(&sub, eid);
found = sidecar.edges.get(&legacy);
}
if let Some(e) = found {
let size = ev.rect.size();
let center = egui::pos2(e.pill_center.0 + base_min.x, e.pill_center.1 + base_min.y);
let min = egui::pos2(center.x - size.x * 0.5, center.y - size.y * 0.5);
ev.rect = egui::Rect::from_min_size(min, size);
if let Some(r) = doc.scene.node_rects.get_mut(eid) { *r = ev.rect; }
}
}
}