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

2 months ago
import dearpygui.dearpygui as dpg
from openpilot.tools.jotpluggler.data import DataManager
1 month ago
from openpilot.tools.jotpluggler.views import TimeSeriesPanel
2 months ago
GRIP_SIZE = 4
MIN_PANE_SIZE = 60
2 months ago
1 month ago
class PlotLayoutManager:
1 month 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
1 month ago
self.worker_manager = worker_manager
2 months ago
self.scale = scale
1 month ago
self.container_tag = "plot_layout_container"
self.active_panels: list = []
2 months ago
1 month ago
initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager)
1 month ago
self.layout: dict = {"type": "panel", "panel": initial_panel}
2 months 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)
2 months 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)
2 months 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)
1 month 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
2 months 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"]
1 month 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
2 months 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
2 months 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
2 months 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)
2 months 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")
2 months ago
def _on_grip_drag(self, sender, app_data, user_data):
path, grip_index, orientation = user_data
layout = self._get_layout_at_path(path)
2 months ago
if "_drag_data" not in layout:
layout["_drag_data"] = {"initial_proportions": layout["proportions"][:], "start_mouse": dpg.get_mouse_pos(local=False)[orientation]}
return
2 months ago
drag_data = layout["_drag_data"]
current_coord = dpg.get_mouse_pos(local=False)[orientation]
delta = current_coord - drag_data["start_mouse"]
2 months 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)))
2 months ago
delta_prop = delta / usable_size
2 months 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
2 months 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)