diff --git a/img2.png b/img2.png new file mode 100644 index 0000000..3b8ed17 Binary files /dev/null and b/img2.png differ diff --git a/negstation/negstation.py b/negstation/negstation.py index ed09258..43f41ea 100644 --- a/negstation/negstation.py +++ b/negstation/negstation.py @@ -13,7 +13,8 @@ from .layout_manager import LayoutManager from .widgets.base_widget import BaseWidget -logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s") +logging.basicConfig(level=logging.DEBUG, + format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) @@ -59,14 +60,16 @@ class EditorManager: and issubclass(cls, ModuleBaseWidget) and cls is not ModuleBaseWidget ): - logging.info(f" -> Found and registered widget: {name}") + logging.info( + f" -> Found and registered widget: {name}") self._register_widget(name, cls) except Exception as e: logging.error(f"Failed to import widget '{py_file.name}': {e}") def _register_widget(self, name: str, widget_class: object): if name in self.widget_classes: - logging.warning(f"Widget '{name}' is already registered. Overwriting.") + logging.warning( + f"Widget '{name}' is already registered. Overwriting.") self.widget_classes[name] = widget_class def _add_widget(self, widget_type: str): @@ -81,7 +84,7 @@ class EditorManager: ) self.layout_manager.load_layout() - dpg.create_viewport(title="NegStation", width=960, height=400) + dpg.create_viewport(title="NegStation", width=800, height=600) dpg.configure_app(docking=True, docking_space=True) with dpg.viewport_menu_bar(): diff --git a/negstation/widgets/stage_viewer_widget.py b/negstation/widgets/stage_viewer_widget.py index 51d69a0..bbeb266 100644 --- a/negstation/widgets/stage_viewer_widget.py +++ b/negstation/widgets/stage_viewer_widget.py @@ -4,223 +4,104 @@ import numpy as np from .base_widget import BaseWidget -# class StageViewerWidget(BaseWidget): -# name: str = "Stage Viewer" - -# def __init__(self, manager, logger): -# super().__init__(manager, logger) -# # ensure texture registry -# if not hasattr(manager, "texture_registry"): -# manager.texture_registry = dpg.add_texture_registry(tag=dpg.generate_uuid()) -# self.registry = manager.texture_registry - -# self.stages = [] -# self.current = None -# self.texture_tag = dpg.generate_uuid() -# self.image_item = None - -# manager.bus.subscribe("pipeline_stages", self.on_stage_list, main_thread=True) -# manager.bus.subscribe("pipeline_stage", self.on_stage_data, main_thread=True) - -# def create(self): -# with dpg.window( -# label="Stage Viewer", -# tag=self.window_tag, -# width=400, -# height=400, -# on_close=self._on_window_close, -# ): -# self.combo = dpg.add_combo(label="Stage", items=[], callback=self.on_select) -# # placeholder 1×1 texture -# dpg.add_dynamic_texture( -# width=1, -# height=1, -# default_value=[0.0, 0.0, 0.0, 0.0], -# tag=self.texture_tag, -# parent=self.registry, -# ) -# self.image_item = dpg.add_image(self.texture_tag) - -# def on_stage_list(self, stages): -# self.stages = stages -# dpg.configure_item(self.combo, items=stages) -# if not self.current and stages: -# self.current = stages[0] -# dpg.set_value(self.combo, self.current) - -# def on_select(self, sender, stage_name): -# self.current = stage_name -# img = self.manager.pipeline.get_stage(stage_name) -# if img is not None: -# self.update_texture(img) - -# def on_stage_data(self, data): -# name, img = data -# if name == self.current: -# self.update_texture(img) - -# def update_texture(self, img: np.ndarray): -# h, w, _ = img.shape -# flat = img.flatten().tolist() - -# # recreate texture at correct size -# if dpg.does_item_exist(self.texture_tag): -# dpg.delete_item(self.texture_tag) -# dpg.add_dynamic_texture( -# width=w, -# height=h, -# default_value=flat, -# tag=self.texture_tag, -# parent=self.registry, -# ) - -# # determine available window size -# win_w, win_h = dpg.get_item_rect_size(self.window_tag) -# # reserve space for combo box (approx 30px) -# available_h = max(win_h - 30, 1) -# # compute scale to fit -# scale = min(win_w / w, available_h / h) - -# disp_w = int(w * scale) -# disp_h = int(h * scale) - -# # update image widget -# dpg.configure_item( -# self.image_item, texture_tag=self.texture_tag, width=disp_w, height=disp_h -# ) - - class StageViewerWidget(BaseWidget): - """ - A robust, zoomable stage viewer using a Dear PyGui Plot to display - dynamic textures without ever deleting them—avoiding segfaults. - """ - name = "Stage Viewer" + name: str = "Image Stage Viewer" def __init__(self, manager, logger): super().__init__(manager, logger) - self.manager.bus.subscribe("pipeline_stages", self._on_stage_list, main_thread=True) - self.manager.bus.subscribe("pipeline_stage", self._on_stage_data, main_thread=True) + # Ensure shared texture registry + if not hasattr(manager, "texture_registry"): + manager.texture_registry = dpg.add_texture_registry( + tag=dpg.generate_uuid()) + self.registry = manager.texture_registry - # one‐time flags and tags - self._initialized = False + self.stages = [] + self.current = None self.texture_tag = dpg.generate_uuid() - self.image_draw_tag = dpg.generate_uuid() - self.plot_tag = dpg.generate_uuid() - self.xaxis_tag = dpg.generate_uuid() - self.yaxis_tag = dpg.generate_uuid() - self.last_size = (1, 1) - self.current_stage = None - self.needs_fit = False + self.img = None + + # Subscribe only to stage list updates + self.manager.bus.subscribe( + "pipeline_stages", self.on_stage_list, main_thread=True) + self.manager.bus.subscribe( + "pipeline_stage", self.on_stage, main_thread=True) def create(self): - if dpg.does_item_exist(self.window_tag): - return + with dpg.window(label="Stage Viewer", tag=self.window_tag, width=400, height=400): + # Dropdown for selecting a stage + self.combo = dpg.add_combo( + label="Stage", items=[], callback=self.on_select) + # Placeholder 1×1 texture in registry + dpg.add_dynamic_texture( + 1, 1, [0, 0, 0, 0], tag=self.texture_tag, parent=self.registry) + # Image widget that will display the texture + self.image_item = dpg.add_image(self.texture_tag) - # ensure a texture registry exists - if not hasattr(self.manager, "texture_registry"): - self.manager.texture_registry = dpg.add_texture_registry(tag=dpg.generate_uuid()) - - with dpg.window(label="Stage Viewer", - tag=self.window_tag, - on_close=self._on_window_close, - width=600, height=600): - - # stage selector - self.combo = dpg.add_combo(label="Stage", items=[], callback=self._on_select) - - # plot container, equal_aspects ensures no distortion - with dpg.plot(label="Image Plot", tag=self.plot_tag, height=-1, width=-1, equal_aspects=True): - self.xaxis_tag = dpg.add_plot_axis(dpg.mvXAxis, - no_tick_labels=True, no_gridlines=True) - self.yaxis_tag = dpg.add_plot_axis(dpg.mvYAxis, - no_tick_labels=True, no_gridlines=True) - - # resize handler to refit on window/plot size changes with dpg.item_handler_registry() as handler: - dpg.add_item_resize_handler(callback=lambda s,a,u: self._fit_image(), user_data=None) + dpg.add_item_resize_handler( + callback=self.on_resize) dpg.bind_item_handler_registry(self.window_tag, handler) - def _on_stage_list(self, stages): + def on_resize(self, app_data): + if self.img is not None: + self.update_texture(self.img) + + def on_stage_list(self, stages): + # Update dropdown items + self.stages = stages dpg.configure_item(self.combo, items=stages) - if not self.current_stage and stages: - self.current_stage = stages[0] - dpg.set_value(self.combo, self.current_stage) - def _on_select(self, sender, stage_name): - self.current_stage = stage_name - img = self.manager.pipeline.get_stage(stage_name) + def on_stage(self, stage): + name, img = stage + if name == self.current: + if img is not None: + self.img = img + self.update_texture(img) + + def on_select(self, sender, selected_stage): + # User-picked stage: fetch and render + self.current = selected_stage + img = self.manager.pipeline.get_stage(selected_stage) if img is not None: - self._update_image(img) + self.img = img + self.update_texture(img) - def _on_stage_data(self, data): - name, img = data - if name == self.current_stage: - self._update_image(img) - - def _update_image(self, img: np.ndarray): + def update_texture(self, img: np.ndarray): + # img is a NumPy array with shape (h, w, 4) h, w, _ = img.shape - self.last_size = (w, h) + flat = img.flatten().tolist() - # First time: create texture & draw-image inside the plot - if not self._initialized: - dpg.add_dynamic_texture(w, h, img, tag=self.texture_tag, - parent=self.manager.texture_registry) - dpg.draw_image(self.texture_tag, - pmin=(0, h), pmax=(w, 0), - tag=self.image_draw_tag, - parent=self.plot_tag) - self._initialized = True - else: - # Subsequent updates: just set_value and adjust draw coords - dpg.set_value(self.texture_tag, img) - dpg.configure_item(self.image_draw_tag, - pmin=(0, self.last_size[1]), - pmax=(self.last_size[0], 0)) + # 1) Recreate the dynamic texture at the correct size + if dpg.does_item_exist(self.texture_tag): + dpg.delete_item(self.texture_tag) + dpg.add_dynamic_texture( + width=w, height=h, + default_value=flat, + tag=self.texture_tag, + parent=self.registry + ) - # show & focus window - dpg.configure_item(self.window_tag, show=True) - dpg.focus_item(self.window_tag) + # 2) Compute available space: full window width, from just below combo to bottom + win_w, win_h = dpg.get_item_rect_size(self.window_tag) + combo_w, combo_h = dpg.get_item_rect_size(self.combo) + combo_x, combo_y = dpg.get_item_pos(self.combo) + avail_w = win_w - 15 + avail_h = win_h - combo_h - combo_y - 15 - # flag to refit axes - self.needs_fit = True + # 3) Compute scale to fit the available rectangle + scale = min(avail_w / w, avail_h / h, 1.0) + disp_w = int(w * scale) + disp_h = int(h * scale) - def _fit_image(self): - """Adjust plot axes so the image fills the available space.""" - if not self._initialized or not self.needs_fit: - return + # 4) Center horizontally, start exactly below the combo + x_off = (avail_w - disp_w) / 2 + 7 + y_off = combo_h + combo_y + 7 # flush immediately below the dropdown - # get plot area size - plot_w = dpg.get_item_width(self.window_tag) - plot_h = dpg.get_item_height(self.window_tag) - 30 # reserve combo height - if plot_w <= 0 or plot_h <= 0: - return - - img_w, img_h = self.last_size - if img_w <= 0 or img_h <= 0: - return - - plot_aspect = plot_w / plot_h - img_aspect = img_w / img_h - - if img_aspect > plot_aspect: - x_min, x_max = 0, img_w - needed_h = img_w / plot_aspect - center_y = img_h / 2 - y_min = center_y - needed_h / 2 - y_max = center_y + needed_h / 2 - else: - y_min, y_max = 0, img_h - needed_w = img_h * plot_aspect - center_x = img_w / 2 - x_min = center_x - needed_w / 2 - x_max = center_x + needed_w / 2 - - dpg.set_axis_limits(self.xaxis_tag, x_min, x_max) - dpg.set_axis_limits(self.yaxis_tag, y_min, y_max) - self.needs_fit = False - - def update(self): - # If we flagged a refit, do it now - if self.needs_fit: - self._fit_image() + # 5) Apply to the image widget + dpg.configure_item( + self.image_item, + texture_tag=self.texture_tag, + pos=(x_off, y_off), + width=disp_w, + height=disp_h + ) diff --git a/negstation_layout.ini b/negstation_layout.ini index 8775e0d..12e1b35 100644 --- a/negstation_layout.ini +++ b/negstation_layout.ini @@ -1,6 +1,6 @@ [Window][WindowOverViewport_11111111] Pos=0,19 -Size=960,381 +Size=810,581 Collapsed=0 [Window][Debug##Default] @@ -25,15 +25,15 @@ Collapsed=0 [Window][###22] Pos=0,19 -Size=207,381 +Size=278,581 Collapsed=0 DockId=0x00000005,0 [Window][###27] -Pos=498,19 -Size=462,381 +Pos=280,19 +Size=530,581 Collapsed=0 -DockId=0x00000004,0 +DockId=0x00000001,0 [Window][###31] Pos=516,19 @@ -45,19 +45,18 @@ DockId=0x00000002,0 Pos=185,19 Size=821,577 Collapsed=0 -DockId=0x00000003,0 +DockId=0x00000001,0 [Window][###38] -Pos=249,45 -Size=192,274 +Pos=209,19 +Size=751,574 Collapsed=0 +DockId=0x00000001,0 [Docking][Data] -DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=960,381 Split=X - DockNode ID=0x00000005 Parent=0x7C6B3D9B SizeRef=207,381 Selected=0xEE087978 - DockNode ID=0x00000006 Parent=0x7C6B3D9B SizeRef=751,381 Split=X - DockNode ID=0x00000001 Parent=0x00000006 SizeRef=514,381 Split=X - DockNode ID=0x00000003 Parent=0x00000001 SizeRef=287,381 CentralNode=1 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=462,381 Selected=0x26E8F608 - DockNode ID=0x00000002 Parent=0x00000006 SizeRef=444,381 Selected=0x62F4D00D +DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=810,581 Split=X + DockNode ID=0x00000005 Parent=0x7C6B3D9B SizeRef=278,381 Selected=0xEE087978 + DockNode ID=0x00000006 Parent=0x7C6B3D9B SizeRef=680,381 Split=X + DockNode ID=0x00000001 Parent=0x00000006 SizeRef=514,381 CentralNode=1 Selected=0x26E8F608 + DockNode ID=0x00000002 Parent=0x00000006 SizeRef=444,381 Selected=0x62F4D00D