openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
12 KiB

1 month ago
import dearpygui.dearpygui as dpg
from openpilot.tools.jotpluggler.data import DataManager
1 month ago
from openpilot.tools.jotpluggler.views import TimeSeriesPanel
1 month ago
GRIP_SIZE = 4
MIN_PANE_SIZE = 60
1 month ago
1 month ago
class PlotLayoutManager:
4 weeks ago
def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0):
1 month ago
self.data_manager = data_manager
self.playback_manager = playback_manager
4 weeks ago
self.worker_manager = worker_manager
1 month ago
self.scale = scale
1 month ago
self.container_tag = "plot_layout_container"
self.active_panels: list = []
1 month ago
4 weeks ago
initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager)
1 month ago
self.layout: dict = {"type": "panel", "panel": initial_panel}
1 month ago
1 month ago
def create_ui(self, parent_tag: str):
if dpg.does_item_exist(self.container_tag):
dpg.delete_item(self.container_tag)
1 month ago
1 month ago
with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True):
container_width, container_height = dpg.get_item_rect_size(self.container_tag)
self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height)
1 month ago
def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
if layout["type"] == "panel":
self._create_panel_ui(layout, parent_tag, path)
else:
self._create_split_ui(layout, parent_tag, path, width, height)
1 month ago
def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]):
panel_tag = self._path_to_tag(path, "panel")
panel = layout["panel"]
self.active_panels.append(panel)
1 month ago
with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True):
with dpg.group(horizontal=True):
dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v))
dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale))
dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale))
dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(40 * self.scale))
dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, 0), width=int(40 * self.scale))
dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, 1), width=int(40 * self.scale))
1 month ago
dpg.add_separator()
content_tag = self._path_to_tag(path, "content")
with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True):
panel.create_ui(content_tag)
def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
split_tag = self._path_to_tag(path, "split")
orientation = layout["orientation"]
min_pane_size = int(MIN_PANE_SIZE * self.scale)
grip_size = int(GRIP_SIZE * self.scale)
num_grips = len(layout["children"]) - 1
with dpg.group(tag=split_tag, parent=parent_tag, horizontal=orientation == 0):
for i, (child_layout, proportion) in enumerate(zip(layout["children"], layout["proportions"], strict=True)):
child_path = path + [i]
container_tag = self._path_to_tag(child_path, "container")
1 month ago
size = [width, height] # pass through since get_item_rect_size is unavailable until rendered
fill_size = [-1, -1] # fill up to the border upon resize
calculated_size = max(min_pane_size, int((size[orientation] - (num_grips * grip_size)) * proportion))
size[orientation] = fill_size[orientation] = calculated_size
with dpg.child_window(tag=container_tag, width=fill_size[0], height=fill_size[1], border=False, no_scrollbar=True):
self._create_ui_recursive(child_layout, container_tag, child_path, size[0], size[1])
if i < len(layout["children"]) - 1: # Add grip between panes (except after the last pane)
self._create_grip(split_tag, path, i, orientation)
def clear_panel(self, panel):
panel.clear()
1 month ago
def delete_panel(self, panel_path: list[int]):
if not panel_path: # Root deletion
old_panel = self.layout["panel"]
old_panel.destroy_ui()
self.active_panels.remove(old_panel)
4 weeks ago
new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager)
1 month ago
self.layout = {"type": "panel", "panel": new_panel}
self._rebuild_ui_at_path([])
return
1 month ago
1 month ago
parent, child_index = self._get_parent_and_index(panel_path)
layout_to_delete = parent["children"][child_index]
self._cleanup_ui_recursive(layout_to_delete, panel_path)
1 month ago
parent["children"].pop(child_index)
parent["proportions"].pop(child_index)
if len(parent["children"]) == 1: # remove parent and collapse
1 month ago
remaining_child = parent["children"][0]
if len(panel_path) == 1: # parent is at root level - promote remaining child to root
1 month ago
self.layout = remaining_child
self._rebuild_ui_at_path([])
else: # replace parent with remaining child in grandparent
1 month ago
grandparent_path = panel_path[:-2]
parent_index = panel_path[-2]
self._replace_layout_at_path(grandparent_path + [parent_index], remaining_child)
self._rebuild_ui_at_path(grandparent_path + [parent_index])
else: # redistribute proportions
1 month ago
equal_prop = 1.0 / len(parent["children"])
parent["proportions"] = [equal_prop] * len(parent["children"])
self._rebuild_ui_at_path(panel_path[:-1])
def split_panel(self, panel_path: list[int], orientation: int):
current_layout = self._get_layout_at_path(panel_path)
existing_panel = current_layout["panel"]
4 weeks ago
new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager)
parent, child_index = self._get_parent_and_index(panel_path)
if parent is None: # Root split
self.layout = {
"type": "split",
"orientation": orientation,
"children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}],
"proportions": [0.5, 0.5],
}
self._rebuild_ui_at_path([])
elif parent["type"] == "split" and parent["orientation"] == orientation: # Same orientation - insert into existing split
parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel})
parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"])
self._rebuild_ui_at_path(panel_path[:-1])
else: # Different orientation - create new split level
new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]}
self._replace_layout_at_path(panel_path, new_split)
self._rebuild_ui_at_path(panel_path)
def _rebuild_ui_at_path(self, path: list[int]):
layout = self._get_layout_at_path(path)
if path:
container_tag = self._path_to_tag(path, "container")
else: # Root update
container_tag = self.container_tag
self._cleanup_ui_recursive(layout, path)
dpg.delete_item(container_tag, children_only=True)
width, height = dpg.get_item_rect_size(container_tag)
self._create_ui_recursive(layout, container_tag, path, width, height)
def _cleanup_ui_recursive(self, layout: dict, path: list[int]):
if layout["type"] == "panel":
panel = layout["panel"]
panel.destroy_ui()
if panel in self.active_panels:
self.active_panels.remove(panel)
else:
for i in range(len(layout["children"]) - 1):
1 month ago
handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler"
if dpg.does_item_exist(handler_tag):
dpg.delete_item(handler_tag)
for i, child in enumerate(layout["children"]):
1 month ago
self._cleanup_ui_recursive(child, path + [i])
1 month ago
def update_all_panels(self):
for panel in self.active_panels:
panel.update()
def on_viewport_resize(self):
self._resize_splits_recursive(self.layout, [])
def _resize_splits_recursive(self, layout: dict, path: list[int]):
if layout["type"] == "split":
split_tag = self._path_to_tag(path, "split")
if dpg.does_item_exist(split_tag):
parent_tag = dpg.get_item_parent(split_tag)
grip_size = int(GRIP_SIZE * self.scale)
min_pane_size = int(MIN_PANE_SIZE * self.scale)
num_grips = len(layout["children"]) - 1
orientation = layout["orientation"]
available_sizes = dpg.get_item_rect_size(parent_tag)
size_properties = ("width", "height")
for i, proportion in enumerate(layout["proportions"]):
child_path = path + [i]
container_tag = self._path_to_tag(child_path, "container")
if dpg.does_item_exist(container_tag):
new_size = max(min_pane_size, int((available_sizes[orientation] - (num_grips * grip_size)) * proportion))
dpg.configure_item(container_tag, **{size_properties[orientation]: new_size})
self._resize_splits_recursive(layout["children"][i], child_path)
1 month ago
def _get_layout_at_path(self, path: list[int]) -> dict:
current = self.layout
for index in path:
current = current["children"][index]
return current
def _get_parent_and_index(self, path: list[int]) -> tuple:
return (None, -1) if not path else (self._get_layout_at_path(path[:-1]), path[-1])
def _replace_layout_at_path(self, path: list[int], new_layout: dict):
if not path:
self.layout = new_layout
else:
parent, index = self._get_parent_and_index(path)
parent["children"][index] = new_layout
1 month ago
1 month ago
def _path_to_tag(self, path: list[int], prefix: str = "") -> str:
path_str = "_".join(map(str, path)) if path else "root"
return f"{prefix}_{path_str}" if prefix else path_str
1 month ago
def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int):
grip_tag = self._path_to_tag(path, f"grip_{grip_index}")
grip_size = int(GRIP_SIZE * self.scale)
width = grip_size if orientation == 0 else -1
height = grip_size if orientation == 1 else -1
1 month ago
with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False):
button_tag = dpg.add_button(label="", width=-1, height=-1)
1 month ago
with dpg.item_handler_registry(tag=f"{grip_tag}_handler"):
user_data = (path, grip_index, orientation)
dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data)
dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data)
dpg.bind_item_handler_registry(button_tag, f"{grip_tag}_handler")
1 month ago
def _on_grip_drag(self, sender, app_data, user_data):
path, grip_index, orientation = user_data
layout = self._get_layout_at_path(path)
1 month ago
if "_drag_data" not in layout:
layout["_drag_data"] = {"initial_proportions": layout["proportions"][:], "start_mouse": dpg.get_mouse_pos(local=False)[orientation]}
return
1 month ago
drag_data = layout["_drag_data"]
current_coord = dpg.get_mouse_pos(local=False)[orientation]
delta = current_coord - drag_data["start_mouse"]
1 month ago
split_tag = self._path_to_tag(path, "split")
if not dpg.does_item_exist(split_tag):
return
total_size = dpg.get_item_rect_size(split_tag)[orientation]
num_grips = len(layout["children"]) - 1
usable_size = max(100, total_size - (num_grips * int(GRIP_SIZE * self.scale)))
1 month ago
delta_prop = delta / usable_size
1 month ago
left_idx = grip_index
right_idx = left_idx + 1
initial = drag_data["initial_proportions"]
min_prop = int(MIN_PANE_SIZE * self.scale) / usable_size
1 month ago
new_left = max(min_prop, initial[left_idx] + delta_prop)
new_right = max(min_prop, initial[right_idx] - delta_prop)
1 month ago
total_available = initial[left_idx] + initial[right_idx]
if new_left + new_right > total_available:
if new_left > new_right:
new_left = total_available - new_right
else:
new_right = total_available - new_left
1 month ago
layout["proportions"] = initial[:]
layout["proportions"][left_idx] = new_left
layout["proportions"][right_idx] = new_right
1 month ago
self._resize_splits_recursive(layout, path)
1 month ago
def _on_grip_end(self, sender, app_data, user_data):
path, _, _ = user_data
self._get_layout_at_path(path).pop("_drag_data", None)