From 5486c3ce37b4af6b8e68dda311dcc2008c8f808d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 25 Mar 2024 18:11:03 -0400 Subject: [PATCH] Start on the foundations of a tree-drawing algorithm I don't actually know what I'm doing. I've done some reading and from that I'm doing experiments until I can better understand what I've read. --- otg/gtk/src/components/review_tree.rs | 178 +++++++++++++++++++++++++- sgf/src/game.rs | 4 +- 2 files changed, 177 insertions(+), 5 deletions(-) diff --git a/otg/gtk/src/components/review_tree.rs b/otg/gtk/src/components/review_tree.rs index f0e3c86..51fc625 100644 --- a/otg/gtk/src/components/review_tree.rs +++ b/otg/gtk/src/components/review_tree.rs @@ -16,9 +16,9 @@ You should have received a copy of the GNU General Public License along with On use cairo::Context; use glib::Object; -use gtk::{ prelude::*, subclass::prelude::*, }; -use sgf::GameRecord; -use std::{rc::Rc, cell::RefCell}; +use gtk::{prelude::*, subclass::prelude::*}; +use sgf::{GameNode, GameRecord}; +use std::{cell::RefCell, rc::Rc}; const WIDTH: i32 = 200; const HEIGHT: i32 = 800; @@ -61,5 +61,177 @@ impl ReviewTree { } pub fn redraw(&self, ctx: &Context, width: i32, height: i32) { + // Implement the tree-drawing algorithm here + } +} + +// https://llimllib.github.io/pymag-trees/ +// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In +// their diagram, they got a tree that looks like this. +// +// O +// |\ +// O O +// |\ \ \ +// O O O O +// |\ |\ +// O O O O +// +// In the same circumstance, what I want is this: +// +// O-- +// | \ +// O O +// |\ |\ +// O O O O +// |\ +// O O +// +// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps +// with another branch, there is some extra drawing space. This might actually be similar to adding +// the principal that "A parent should be centered over its children". +// +// So, given a tree, I need to know how many children exist at each level. Then I build parents +// atop the children. At level 3, I have four children, and that happens to be the maximum width of +// the graph. +// +// A bottom-up traversal: +// - Figure out the number of nodes at each depth + +/* +struct Tree { + width: Vec, // the total width of the tree at each depth +} +*/ + +// Given a node, do a postorder traversal to figure out the width of the node based on all of its +// children. This is equivalent to the widest of all of its children at all depths. +// +// There are some collapse rules that I could take into account here, but that I haven't figured +// out yet. If two nodes are side by side, and one of them has some wide children but the other has +// no children, then they are effectively the same width. The second node only needs to be moved +// out if it has children that would overlap the children of the first node. +// +// My algorithm right now is likely to generate unnecessarily wide trees in a complex game review. +fn node_width(node: &GameNode) -> usize { + let children: &Vec = match node { + GameNode::MoveNode(mn) => &mn.children, + GameNode::SetupNode(sn) => &sn.children, + }; + + if children.len() == 0 { + return 1; + } + + // If there is more than one child, run node_width on each one and add them together. + children.iter().fold(0, |acc, child| acc + node_width(child)) +} + +// Since I know the width of a node, now I want to figure out its placement in the larger scheme of +// things. +// +// One thought I have is that I could just develop a grid virtually and start placing nodes. +// Whenever I notice a collision, I can just move the node over. But I'd like to see if I can be a +// bit smarter than doing it as just a vec into which I place things, as though it's a game board. +// So, given a game node, I want to figure out it's position along the X axis. +// +// Just having the node is greatly insufficient. I can get better results if I'm calculating the +// position of its children. +fn node_children_columns(node: &GameNode) -> Vec { + vec![0, 1, 2] +} + +#[cfg(test)] +mod test { + use super::*; + use sgf::{Color, GameNode, Move, MoveNode}; + + #[test] + fn it_calculates_width_for_single_node() { + let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + + assert_eq!(node_width(&node), 1); + } + + #[test] + fn it_calculates_width_for_node_with_children() { + let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + + node_a.children.push(node_b); + node_a.children.push(node_c); + node_a.children.push(node_d); + + assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3); + } + + // A + // B E + // C D + #[test] + fn it_calculates_width_with_one_deep_child() { + let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + + node_b.children.push(GameNode::MoveNode(node_c)); + node_b.children.push(GameNode::MoveNode(node_d)); + assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2); + + node_a.children.push(GameNode::MoveNode(node_b)); + node_a.children.push(GameNode::MoveNode(node_e)); + assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3); + } + + // A + // B G H + // C I + // D E F + #[test] + fn it_calculates_a_complex_tree() { + let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let mut node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + + node_c.children.push(GameNode::MoveNode(node_d)); + node_c.children.push(GameNode::MoveNode(node_e)); + node_c.children.push(GameNode::MoveNode(node_f)); + assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3); + + node_b.children.push(GameNode::MoveNode(node_c)); + assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3); + + node_h.children.push(GameNode::MoveNode(node_i)); + + node_a.children.push(GameNode::MoveNode(node_b)); + node_a.children.push(GameNode::MoveNode(node_g)); + node_a.children.push(GameNode::MoveNode(node_h)); + // This should be 4 if I were collapsing levels correctly, but it is 5 until I return to + // figure that step out. + assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5); + } + + #[test] + fn a_nodes_children_get_separate_columns() { + let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned())); + let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned()))); + + node_a.children.push(node_b.clone()); + node_a.children.push(node_c.clone()); + node_a.children.push(node_d.clone()); + + assert_eq!(node_children_columns(&GameNode::MoveNode(node_a)), vec![0, 1, 2]); } } diff --git a/sgf/src/game.rs b/sgf/src/game.rs index e30126a..1343565 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -460,8 +460,8 @@ impl TryFrom<&parser::Node> for MoveNode { pub struct SetupNode { id: Uuid, - positions: Vec, - children: Vec, + pub positions: Vec, + pub children: Vec, } impl SetupNode {