summaryrefslogtreecommitdiff
path: root/ui/windows/plot_window.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-06-03 16:17:58 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-06-03 16:17:58 -0600
commit903df67b153ca4af877ba0b534d4802d74cf34df (patch)
tree9f0dcef93bf87979b7168d2c3f94fd395effc03a /ui/windows/plot_window.py
parent216a278102e3f63f155d76d62790a3ca60fcec02 (diff)
Equalize tile sizes after every BSP tree operation
After each drag-drop, add, or remove, walk the tree and reset each split's ratio to left_leaves/(left_leaves+right_leaves). This distributes space equally per pane regardless of the prior tree structure. - _tree_equalize_ratios: recomputes all ratios bottom-up by leaf count - _tree_leaf_count: counts leaves in a subtree - _tree_grid_size: uses LCM of split denominators (capped at 12) instead of 2^depth, giving exact integer rowspans for equal distributions - _tree_default: balanced binary tree + equalize (not uneven chain) - Applied in _do_drop, _add_pane, _rm Result: 3 side-by-side panes → equal widths; any mixed layout → equal area. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'ui/windows/plot_window.py')
-rw-r--r--ui/windows/plot_window.py76
1 files changed, 59 insertions, 17 deletions
diff --git a/ui/windows/plot_window.py b/ui/windows/plot_window.py
index b933df1..80a1624 100644
--- a/ui/windows/plot_window.py
+++ b/ui/windows/plot_window.py
@@ -19,6 +19,7 @@ from __future__ import annotations
import json
from copy import deepcopy
from dataclasses import dataclass, field, asdict
+from math import gcd
from typing import Dict, List, Optional, Tuple
from PyQt6.QtWidgets import (
@@ -120,20 +121,22 @@ def build_default_layout(registry: DeviceRegistry,
# hsplit = side-by-side (first=left, second=right)
# vsplit = top-bottom (first=top, second=bottom)
+def _tree_default_range(indices: List[int]) -> dict:
+ if len(indices) == 1:
+ return {"kind": "leaf", "pane": indices[0]}
+ mid = len(indices) // 2
+ return {"kind": "vsplit", "ratio": 0.5,
+ "first": _tree_default_range(indices[:mid]),
+ "second": _tree_default_range(indices[mid:])}
+
+
def _tree_default(n: int) -> Optional[dict]:
- """Build default equal vsplit chain for n panes."""
+ """Build balanced binary vsplit tree with equal ratios for n panes."""
if n == 0:
return None
- leaves = [{"kind": "leaf", "pane": i} for i in range(n)]
if n == 1:
- return leaves[0]
- node = leaves[-1]
- for i in range(n - 2, -1, -1):
- remaining = n - i
- ratio = 1.0 / remaining
- node = {"kind": "vsplit", "ratio": ratio,
- "first": leaves[i], "second": node}
- return node
+ return {"kind": "leaf", "pane": 0}
+ return _tree_equalize_ratios(_tree_default_range(list(range(n))))
def _tree_depth(node: dict) -> int:
@@ -186,6 +189,32 @@ def _tree_reindex(node: dict, removed_idx: int) -> dict:
"second": _tree_reindex(node["second"], removed_idx)}
+def _tree_leaf_count(node: dict) -> int:
+ if node["kind"] == "leaf":
+ return 1
+ return _tree_leaf_count(node["first"]) + _tree_leaf_count(node["second"])
+
+
+def _tree_equalize_ratios(node: dict) -> dict:
+ """Reset each split's ratio so both subtrees get equal space per leaf."""
+ if node["kind"] == "leaf":
+ return node
+ first = _tree_equalize_ratios(node["first"])
+ second = _tree_equalize_ratios(node["second"])
+ n1 = _tree_leaf_count(first)
+ n2 = _tree_leaf_count(second)
+ return {**node, "ratio": n1 / (n1 + n2), "first": first, "second": second}
+
+
+def _collect_denoms(node: dict, out: List[int]) -> None:
+ if node["kind"] != "leaf":
+ n1 = _tree_leaf_count(node["first"])
+ n2 = _tree_leaf_count(node["second"])
+ out.append(n1 + n2)
+ _collect_denoms(node["first"], out)
+ _collect_denoms(node["second"], out)
+
+
def tree_to_grid(node: dict, row: int, col: int, rowspan: int, colspan: int,
result: Dict[int, Tuple[int, int, int, int]]) -> None:
"""Recursively compute (row, col, rowspan, colspan) for each leaf."""
@@ -206,8 +235,17 @@ def tree_to_grid(node: dict, row: int, col: int, rowspan: int, colspan: int,
def _tree_grid_size(node: dict) -> int:
- """Grid dimension = 2^depth, capped at 8."""
- return 2 ** min(_tree_depth(node), 3)
+ """Minimal grid = LCM of all split denominators (leaf counts), capped at 12."""
+ denoms: List[int] = []
+ _collect_denoms(node, denoms)
+ if not denoms:
+ return 1
+ result = 1
+ for d in denoms:
+ result = result * d // gcd(result, d)
+ if result >= 12:
+ return 12
+ return result
# ══════════════════════════════════════════════════════════════════════════════
@@ -605,7 +643,7 @@ class LayoutCanvas(QFrame):
# Only one pane
self._reposition_tiles()
return
- new_tree = _tree_insert(new_tree, target_idx, dragged_idx, zone)
+ new_tree = _tree_equalize_ratios(_tree_insert(new_tree, target_idx, dragged_idx, zone))
self._tree = new_tree
if self._cfg_ref is not None:
self._cfg_ref.tree = new_tree
@@ -770,9 +808,11 @@ class PlotWindow(QWidget):
if self.cfg.tree is None:
self.cfg.tree = {"kind": "leaf", "pane": 0}
else:
- self.cfg.tree = {"kind": "vsplit", "ratio": 0.5,
- "first": self.cfg.tree,
- "second": {"kind": "leaf", "pane": new_idx}}
+ self.cfg.tree = _tree_equalize_ratios({
+ "kind": "vsplit", "ratio": 0.5,
+ "first": self.cfg.tree,
+ "second": {"kind": "leaf", "pane": new_idx},
+ })
self._insert(spec)
self.cfg.layout_mode = "free"
self._canvas.sync_panes(self.cfg.panes, self.cfg.tree)
@@ -786,7 +826,9 @@ class PlotWindow(QWidget):
if self.cfg.tree:
self.cfg.tree = _tree_remove(self.cfg.tree, removed_idx)
if self.cfg.tree:
- self.cfg.tree = _tree_reindex(self.cfg.tree, removed_idx)
+ self.cfg.tree = _tree_equalize_ratios(
+ _tree_reindex(self.cfg.tree, removed_idx)
+ )
self._canvas.sync_panes(self.cfg.panes, self.cfg.tree)
self._maybe_live()