New pipeline widget framework
This commit is contained in:
@ -11,24 +11,68 @@ class BaseWidget:
|
||||
name: str = "BaseWidget"
|
||||
register: bool = False
|
||||
|
||||
def __init__(self, manager: "EditorManager", logger: logging.Logger):
|
||||
def __init__(
|
||||
self,
|
||||
manager: "EditorManager",
|
||||
logger: logging.Logger,
|
||||
window_width: int = 300,
|
||||
window_height: int = 200,
|
||||
):
|
||||
self.manager = manager
|
||||
self.logger = logger
|
||||
self.window_width = window_width
|
||||
self.window_height = window_height
|
||||
self.window_offset_x = 0
|
||||
self.window_offset_y = 0
|
||||
self.window_tag = dpg.generate_uuid()
|
||||
self.config = {}
|
||||
|
||||
def create(self):
|
||||
"""Called by negstation itself, creates the window"""
|
||||
with dpg.window(
|
||||
label=self.name,
|
||||
tag=self.window_tag,
|
||||
width=self.window_width,
|
||||
height=self.window_height,
|
||||
on_close=self._on_window_close,
|
||||
):
|
||||
self.window_handler = dpg.add_item_handler_registry()
|
||||
dpg.add_item_resize_handler(
|
||||
callback=self._on_window_resize, parent=self.window_handler
|
||||
)
|
||||
dpg.bind_item_handler_registry(self.window_tag, self.window_handler)
|
||||
|
||||
self.create_content()
|
||||
|
||||
def create_content(self):
|
||||
"""Must be implemented by the widget, creates the content of the window"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self):
|
||||
"""Must be implemented by the widget, is called in the render loop every frame"""
|
||||
pass
|
||||
|
||||
def on_resize(self, width: int, height: int):
|
||||
"""Must be implemented by the widget, is called after a resize"""
|
||||
pass
|
||||
|
||||
# Internal but public funtions
|
||||
|
||||
def get_config(self):
|
||||
"""Caled by negstation itself, returns the saved widget config"""
|
||||
return self.config
|
||||
|
||||
# Callbacks
|
||||
|
||||
def _on_window_close(self):
|
||||
try:
|
||||
dpg.delete_item(self.window_tag)
|
||||
self.manager.widgets.remove(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _on_window_resize(self, data):
|
||||
win_w, win_h = dpg.get_item_rect_size(self.window_tag)
|
||||
self.window_height = win_h
|
||||
self.window_width = win_w
|
||||
self.on_resize(win_w, win_h)
|
||||
|
@ -1,25 +1,24 @@
|
||||
import dearpygui.dearpygui as dpg
|
||||
import numpy as np
|
||||
|
||||
from .pipeline_stage_widget import PipelineStageWidget
|
||||
|
||||
|
||||
class InvertStage(PipelineStageWidget):
|
||||
name = "Invert Image"
|
||||
register = True
|
||||
has_pipeline_in = True
|
||||
has_pipeline_out = True
|
||||
|
||||
def __init__(self, manager, logger):
|
||||
super().__init__(manager, logger)
|
||||
self.stage_out = "inverted_image"
|
||||
super().__init__(manager, logger, default_stage_out="inverted_image")
|
||||
|
||||
def create_pipeline_stage_content(self):
|
||||
dpg.add_text("Inversion is happening here")
|
||||
|
||||
def create_content(self):
|
||||
dpg.add_button(label="Invert", callback=lambda s, a, u: self._do_invert())
|
||||
|
||||
def on_stage(self, img):
|
||||
self._do_invert()
|
||||
|
||||
def _do_invert(self):
|
||||
if self.img is None:
|
||||
def on_pipeline_data(self, img):
|
||||
if img is None:
|
||||
return
|
||||
inverted = self.img.copy()
|
||||
inverted = img.copy()
|
||||
inverted[...,:3] = 1.0 - inverted[...,:3]
|
||||
self.publish(inverted)
|
||||
self.publish_stage(inverted)
|
||||
|
@ -2,20 +2,21 @@ import dearpygui.dearpygui as dpg
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
from .base_widget import BaseWidget
|
||||
from .pipeline_stage_widget import PipelineStageWidget
|
||||
|
||||
|
||||
class OpenImageWidget(BaseWidget):
|
||||
name: str = "Open Image"
|
||||
class OpenImageWidget(PipelineStageWidget):
|
||||
name = "Open Image"
|
||||
register = True
|
||||
has_pipeline_in = False
|
||||
has_pipeline_out = True
|
||||
|
||||
def __init__(self, manager, logger, stage_out="loaded_image"):
|
||||
super().__init__(manager, logger)
|
||||
self.stage_out = stage_out
|
||||
def __init__(self, manager, logger):
|
||||
super().__init__(manager, logger, default_stage_out="opened_image")
|
||||
self.dialog_tag = dpg.generate_uuid()
|
||||
self.output_tag = dpg.generate_uuid()
|
||||
|
||||
def create(self):
|
||||
def create_pipeline_stage_content(self):
|
||||
with dpg.file_dialog(
|
||||
directory_selector=False,
|
||||
show=False,
|
||||
@ -25,24 +26,12 @@ class OpenImageWidget(BaseWidget):
|
||||
width=400,
|
||||
):
|
||||
dpg.add_file_extension(".*")
|
||||
|
||||
with dpg.window(
|
||||
label="Open Image File",
|
||||
tag=self.window_tag,
|
||||
width=300,
|
||||
height=150,
|
||||
on_close=self._on_window_close,
|
||||
):
|
||||
dpg.add_input_text(label="Stage Output Name", tag=self.output_tag)
|
||||
dpg.add_button(label="Open File...", callback=self._on_open_file)
|
||||
|
||||
dpg.set_value(self.output_tag, self.stage_out)
|
||||
dpg.add_button(label="Open File...", callback=self._on_open_file)
|
||||
|
||||
def _on_open_file(self):
|
||||
dpg.configure_item(self.dialog_tag, show=True)
|
||||
|
||||
def _on_file_selected(self, sender, app_data):
|
||||
# app_data[0] is dict with selected file paths
|
||||
selection = (
|
||||
f"{app_data['current_path']}/{list(app_data['selections'].keys())[0]}"
|
||||
if isinstance(app_data, dict)
|
||||
@ -55,6 +44,6 @@ class OpenImageWidget(BaseWidget):
|
||||
img = Image.open(selection).convert("RGBA")
|
||||
arr = np.asarray(img).astype(np.float32) / 255.0 # normalize to [0,1]
|
||||
# Publish into pipeline
|
||||
self.manager.pipeline.add_stage(dpg.get_value(self.output_tag), arr)
|
||||
self.manager.pipeline.publish(self.pipeline_stage_out_id, arr)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to load image {selection}: {e}")
|
||||
|
@ -4,60 +4,100 @@ from .base_widget import BaseWidget
|
||||
|
||||
|
||||
class PipelineStageWidget(BaseWidget):
|
||||
name = "Pipeline Stage Widget"
|
||||
register = False
|
||||
has_pipeline_in: bool = False
|
||||
has_pipeline_out: bool = False
|
||||
|
||||
def __init__(self, manager, logger):
|
||||
super().__init__(manager, logger)
|
||||
self.stage_in = ""
|
||||
self.stage_out = ""
|
||||
self.img = None
|
||||
def __init__(
|
||||
self,
|
||||
manager,
|
||||
logger,
|
||||
default_stage_in: str = "pipeline_in",
|
||||
default_stage_out: str = "pipeline_out",
|
||||
window_width: int = 300,
|
||||
window_height: int = 200,
|
||||
):
|
||||
super().__init__(manager, logger, window_width, window_height)
|
||||
self.pipeline_stage_in_id = None
|
||||
self.pipeline_stage_out_id = None
|
||||
self.pipeline_config_group_tag = dpg.generate_uuid()
|
||||
|
||||
self.manager.bus.subscribe(
|
||||
"pipeline_stage", self._on_pipeline, main_thread=True
|
||||
)
|
||||
self.manager.bus.subscribe(
|
||||
"pipeline_stages", self._on_stage_list, main_thread=True
|
||||
)
|
||||
|
||||
def create(self):
|
||||
with dpg.window(label=self.name, tag=self.window_tag, width=400, height=300):
|
||||
# top‐row: input / output
|
||||
dpg.add_text("Configuration:")
|
||||
self.combo = dpg.add_combo(
|
||||
label="Stage In", items=[], callback=self._on_select
|
||||
if self.has_pipeline_out:
|
||||
self.pipeline_stage_out_id = self.manager.pipeline.register_stage(
|
||||
default_stage_out
|
||||
)
|
||||
dpg.add_input_text(
|
||||
label="Stage Out",
|
||||
default_value=self.stage_out,
|
||||
callback=lambda s, a, u: setattr(self, "stage_out", a),
|
||||
)
|
||||
dpg.add_separator()
|
||||
# now let subclasses populate the rest
|
||||
self.create_content()
|
||||
|
||||
def _on_select(self, sender, selected_stage):
|
||||
self.stage_in = selected_stage
|
||||
self.img = self.manager.pipeline.get_stage(selected_stage)
|
||||
self.on_stage(self.img)
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
self.manager.bus.subscribe("pipeline_stages", self._on_stage_list, True)
|
||||
if self.has_pipeline_in:
|
||||
self.pipeline_stage_in_id = 0
|
||||
self.manager.bus.subscribe("pipeline_stage", self._on_stage_data, True)
|
||||
# force getting all available pipeline stages
|
||||
self.manager.pipeline.republish_stages()
|
||||
|
||||
def create_content(self):
|
||||
with dpg.group(tag=self.pipeline_config_group_tag):
|
||||
if self.has_pipeline_in:
|
||||
self.stage_in_combo = dpg.add_combo(
|
||||
label="Stage In",
|
||||
items=[],
|
||||
callback=self._on_stage_in_select,
|
||||
default_value=f"{self.manager.pipeline.get_stage_name(0)} : 0",
|
||||
)
|
||||
if self.has_pipeline_out:
|
||||
dpg.add_input_text(
|
||||
label="Stage Out",
|
||||
default_value=self.manager.pipeline.get_stage_name(
|
||||
self.pipeline_stage_out_id
|
||||
),
|
||||
callback=lambda s, a, u: self.manager.pipeline.rename_stage(
|
||||
self.pipeline_stage_out_id, a
|
||||
),
|
||||
)
|
||||
dpg.add_separator()
|
||||
self.create_pipeline_stage_content()
|
||||
|
||||
def publish_stage(self, img: np.ndarray):
|
||||
"""Publishes an image to output stage"""
|
||||
if self.has_pipeline_out:
|
||||
self.manager.pipeline.publish(self.pipeline_stage_out_id, img)
|
||||
|
||||
def create_pipeline_stage_content(self):
|
||||
"""Must be implemented by the widget, creates the content of the window"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_pipeline(self, data):
|
||||
name, img = data
|
||||
if name == self.stage_in:
|
||||
self.img = img
|
||||
self.on_stage(img)
|
||||
|
||||
def _on_stage_list(self, stages):
|
||||
self.stages = stages
|
||||
dpg.configure_item(self.combo, items=stages)
|
||||
|
||||
def on_stage(self, img: np.ndarray):
|
||||
def on_pipeline_data(self, img: np.ndarray):
|
||||
"""Must be implemented by the widget, is called when there is a new image published on the in stage"""
|
||||
pass
|
||||
|
||||
def publish(self, img: np.ndarray):
|
||||
self.manager.pipeline.add_stage(self.stage_out, img)
|
||||
# Callbacks
|
||||
|
||||
def _on_stage_list(self, stagelist):
|
||||
if self.has_pipeline_in:
|
||||
stages = [f"{stage} : {id}" for id, stage in enumerate(stagelist)]
|
||||
dpg.configure_item(self.stage_in_combo, items=stages)
|
||||
|
||||
def _on_stage_in_select(self, sender, selected_stage: str):
|
||||
d = selected_stage.split(" : ")
|
||||
name = d[0]
|
||||
id = int(d[1])
|
||||
self.pipeline_stage_in_id = id
|
||||
if self.has_pipeline_in:
|
||||
img = self.manager.pipeline.get_stage_data(id)
|
||||
self.on_pipeline_data(img)
|
||||
|
||||
def _on_stage_data(self, data):
|
||||
pipeline_id = data[0]
|
||||
img = data[1]
|
||||
if self.has_pipeline_in and pipeline_id == self.pipeline_stage_in_id:
|
||||
self.on_pipeline_data(img)
|
||||
|
||||
# Override the window resize callback
|
||||
def _on_window_resize(self, data):
|
||||
win_w, win_h = dpg.get_item_rect_size(self.window_tag)
|
||||
group_w, group_h = dpg.get_item_rect_size(self.pipeline_config_group_tag)
|
||||
group_x, group_y = dpg.get_item_pos(self.pipeline_config_group_tag)
|
||||
self.window_height = win_h - group_h - group_y - 12
|
||||
self.window_width = win_w - 7
|
||||
self.window_offset_y = group_h + group_y + 3
|
||||
self.on_resize(win_w, win_h)
|
||||
|
@ -1,114 +1,72 @@
|
||||
import dearpygui.dearpygui as dpg
|
||||
import numpy as np
|
||||
|
||||
from .base_widget import BaseWidget
|
||||
from .pipeline_stage_widget import PipelineStageWidget
|
||||
|
||||
|
||||
class StageViewerWidget(BaseWidget):
|
||||
name: str = "Image Stage Viewer"
|
||||
class PipelineStageViewer(PipelineStageWidget):
|
||||
name = "View Image"
|
||||
register = True
|
||||
has_pipeline_in = True
|
||||
has_pipeline_out = False
|
||||
|
||||
def __init__(self, manager, logger):
|
||||
super().__init__(manager, logger)
|
||||
# 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
|
||||
|
||||
self.stages = []
|
||||
self.current = None
|
||||
super().__init__(manager, logger, default_stage_in="pipeline_out")
|
||||
self.texture_tag = dpg.generate_uuid()
|
||||
self.img = None
|
||||
self.needs_update = False
|
||||
self.registry = manager.texture_registry
|
||||
|
||||
# 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_pipeline_stage_content(self):
|
||||
dpg.add_dynamic_texture(
|
||||
1, 1, [0, 0, 0, 0], tag=self.texture_tag, parent=self.registry
|
||||
)
|
||||
self.image_item = dpg.add_image(self.texture_tag)
|
||||
|
||||
def create(self):
|
||||
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)
|
||||
def on_resize(self, width, height):
|
||||
self.needs_update = True
|
||||
|
||||
with dpg.item_handler_registry() as handler:
|
||||
dpg.add_item_resize_handler(
|
||||
callback=self.on_resize)
|
||||
dpg.bind_item_handler_registry(self.window_tag, handler)
|
||||
|
||||
def on_resize(self, app_data):
|
||||
if self.img is not None:
|
||||
self.needs_update = True
|
||||
# self.update_texture(self.img)
|
||||
|
||||
def on_stage_list(self, stages):
|
||||
# Update dropdown items
|
||||
self.stages = stages
|
||||
dpg.configure_item(self.combo, items=stages)
|
||||
|
||||
def on_stage(self, stage):
|
||||
name, img = stage
|
||||
if name == self.current:
|
||||
if img is not None:
|
||||
self.img = img
|
||||
self.needs_update = True
|
||||
# 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.img = img
|
||||
self.needs_update = True
|
||||
# self.update_texture(img)
|
||||
def on_pipeline_data(self, img):
|
||||
self.img = img
|
||||
self.needs_update = True
|
||||
|
||||
def update_texture(self, img: np.ndarray):
|
||||
# img is a NumPy array with shape (h, w, 4)
|
||||
"""Only call from update function"""
|
||||
if img is None:
|
||||
dpg.configure_item(self.image_item, show=False)
|
||||
return
|
||||
|
||||
h, w, _ = img.shape
|
||||
flat = img.flatten().tolist()
|
||||
|
||||
# 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,
|
||||
width=w,
|
||||
height=h,
|
||||
default_value=flat,
|
||||
tag=self.texture_tag,
|
||||
parent=self.registry
|
||||
parent=self.registry,
|
||||
)
|
||||
|
||||
# 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
|
||||
win_w, win_h = self.window_width, self.window_height
|
||||
avail_w = win_w
|
||||
avail_h = win_h
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
x_off = (avail_w - disp_w) / 2
|
||||
y_off = self.window_offset_y
|
||||
|
||||
# 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
|
||||
height=disp_h,
|
||||
show=True
|
||||
)
|
||||
|
||||
def update(self):
|
||||
|
Reference in New Issue
Block a user