From 35df42f3a73d09cef02c818670de56783fc2b72f Mon Sep 17 00:00:00 2001 From: Jonas Lukasczyk Date: Wed, 7 Aug 2024 16:52:56 +0200 Subject: [PATCH 1/2] input-input connections; id based selection fixes; open path button in node editor --- pycinema/Core.py | 5 + pycinema/filters/RenderView.py | 132 ++++++++++++++++++++++ pycinema/filters/TableView.py | 51 ++++++--- pycinema/filters/__init__.py | 1 + pycinema/theater/Icons.py | 1 + pycinema/theater/Theater.py | 12 +- pycinema/theater/node_editor/Edge.py | 49 ++++++-- pycinema/theater/node_editor/InputText.py | 10 ++ pycinema/theater/node_editor/Port.py | 41 +++++-- pycinema/theater/views/NodeEditorView.py | 45 ++++---- 10 files changed, 283 insertions(+), 64 deletions(-) create mode 100644 pycinema/filters/RenderView.py diff --git a/pycinema/Core.py b/pycinema/Core.py index 29983724..653fa365 100644 --- a/pycinema/Core.py +++ b/pycinema/Core.py @@ -192,9 +192,14 @@ def getTableExtent(table): # Image Class ################################################################################ class Image(): + image_id_counter = -1 + def __init__(self, channels=None, meta=None): self.meta = meta or {} self.channels = channels or {} + if 'id' not in self.meta: + self.meta['id'] = Image.image_id_counter + Image.image_id_counter += -1 def __str__(self): result = '{ PyCinemaImage:\n' diff --git a/pycinema/filters/RenderView.py b/pycinema/filters/RenderView.py new file mode 100644 index 00000000..8da7fe5f --- /dev/null +++ b/pycinema/filters/RenderView.py @@ -0,0 +1,132 @@ +from pycinema import Filter + +import numpy +import logging as log + +try: + from PySide6 import QtGui, QtCore, QtWidgets +except ImportError: + pass + +try: + class _QGraphicsPixmapItem(QtWidgets.QGraphicsPixmapItem): + def __init__(self): + super().__init__() + + self.setShapeMode(QtWidgets.QGraphicsPixmapItem.BoundingRectShape) + self.setTransformationMode(QtCore.Qt.SmoothTransformation) + + def update(self,rgba): + qimage = QtGui.QImage( + rgba, + rgba.shape[1], rgba.shape[0], + rgba.shape[1] * 4, + QtGui.QImage.Format_RGBA8888 + ) + self.setPixmap( QtGui.QPixmap(qimage) ) + + def paint(self, painter, option, widget=None): + super().paint(painter, option, widget) + + class _ImageViewer(QtWidgets.QGraphicsView): + + def __init__(self,filter): + super().__init__() + + self.filter = filter + + self.mode = 0 + self.mouse_pos_0 = None + self.camera_0 = None + + self.setRenderHints(QtGui.QPainter.Antialiasing) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setScene(filter.scene) + + def fitInView(self): + rect = QtCore.QRectF(self.scene().itemsBoundingRect()) + if rect.isNull(): + return + self.resetTransform() + super().fitInView(rect, QtCore.Qt.KeepAspectRatio) + + def resizeEvent(self, event): + super().resizeEvent(event) + self.fitInView() + + def wheelEvent(self, event): + return + + def keyPressEvent(self,event): + if event.key()==32: + self.fitInView() + + def mousePressEvent(self,event): + self.mode = 1 + self.mouse_pos_0 = event.pos() + self.camera_0 = self.filter.inputs.camera.get() + super().mousePressEvent(event) + + def mouseMoveEvent(self,event): + if self.mode != 1: return + + delta = event.pos() - self.mouse_pos_0 + factor = 0.1 + camera_1 = [ + # round(sorted([0, 360, self.camera_0[0] + delta.x()*factor])[1], 2), + round((self.camera_0[0] + delta.x()*factor), 2), + round(sorted([-90, 90, self.camera_0[1] + delta.y()*factor])[1], 2), + ] + self.filter.inputs.camera.set(camera_1,True,True) + + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self,event): + if self.mode != 1: return + + self.mode = 0 + super().mouseReleaseEvent(event) + +except NameError: + pass + +class RenderView(Filter): + + def __init__(self): + self.scene = QtWidgets.QGraphicsScene() + + self.canvas = _QGraphicsPixmapItem() + self.scene.addItem(self.canvas) + + self.widgets = [] + + Filter.__init__( + self, + inputs={ + 'images': [], + 'camera': [0,0] + }, + outputs={ + 'images': [] + } + ) + + def generateWidgets(self): + widget = _ImageViewer(self) + self.widgets.append(widget) + return widget + + def _update(self): + images = self.inputs.images.get() + if len(images)<1 or 'rgba' not in images[0].channels: + print('[WARNING:RenderView] No input image or missing rgba channel') + self.canvas.update(numpy.zeros((1,1,4))) + return 1 + + self.canvas.update(images[0].channels['rgba']) + + for w in self.widgets: + w.fitInView() + + return 1 diff --git a/pycinema/filters/TableView.py b/pycinema/filters/TableView.py index 937e2b72..768394cf 100644 --- a/pycinema/filters/TableView.py +++ b/pycinema/filters/TableView.py @@ -63,11 +63,13 @@ def __init__(self): self.selection_model.setModel(self.proxyModel) self.selection_model.selectionChanged.connect(self._onSelectionChanged) - self.update_from_selection = False self.suppress_selection_update = False self.outputTable = list() + self.widgets = [] + self.table_input_time = -1 + Filter.__init__( self, inputs={ @@ -85,12 +87,12 @@ def generateWidgets(self): widget.setSortingEnabled(True) widget.setSelectionModel(self.selection_model) widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.widgets.append(widget) return widget def _onSelectionChanged(self, selected, deselected): if self.suppress_selection_update: return - self.update_from_selection = True indices = set(self.proxyModel.mapToSource(index).row() for index in self.selection_model.selectedIndexes()) table = self.inputs.table.get() @@ -108,32 +110,45 @@ def _update(self): table = self.inputs.table.get() input_is_image_list = len(table)>0 and isinstance(table[0],Image) - if not self.update_from_selection: - table_data = table - if input_is_image_list: - table_data = ImagesToTable.imagesToTable(table) - self.model.setData(table_data) - self.update_from_selection = False + if self.table_input_time != self.inputs.table.getTime(): + self.table_input_time = self.inputs.table.getTime() + table_data = table + if input_is_image_list: + table_data = ImagesToTable.imagesToTable(table) + self.model.setData(table_data) selection = self.inputs.selection.get() - selection_indices = None - output_table = None + selection_indices = [] + output_table = [[]] if input_is_image_list: selection_indices = [i for i in range(0,len(table)) if table[0].meta['id'] in selection] output_table = [table[i] for i in selection_indices] else: - id_column_idx = table[0].index('id') - selection_indices = [i for i in range(0,len(table)) if table[i][id_column_idx] in selection] - output_table = [table[0]] - for i in selection_indices: - output_table.append(table[i]) + try: id_column_idx = table[0].index('id') + except ValueError: id_column_idx = -1 + + selection_mode = QtWidgets.QAbstractItemView.ExtendedSelection + if id_column_idx<0: + selection_mode = QtWidgets.QAbstractItemView.NoSelection + + else: + selection_indices = [i for i in range(0,len(table)) if table[i][id_column_idx] in selection] + output_table = [table[0]] + for i in selection_indices: + output_table.append(table[i]) + for w in self.widgets: + w.setSelectionMode(selection_mode) self.outputs.table.set( output_table ) self.suppress_selection_update = True - indices_ = [self.model.index(r-1, 0) for r in selection_indices] - mode = QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows - [self.selection_model.select(self.proxyModel.mapFromSource(i), mode) for i in indices_] + if id_column_idx<0: + self.selection_model.clear() + self.inputs.selection.set([]) + else: + indices_ = [self.model.index(r-1, 0) for r in selection_indices] + mode = QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows + [self.selection_model.select(self.proxyModel.mapFromSource(i), mode) for i in indices_] self.suppress_selection_update = False return 1 diff --git a/pycinema/filters/__init__.py b/pycinema/filters/__init__.py index 0f85946f..ae48f1c9 100644 --- a/pycinema/filters/__init__.py +++ b/pycinema/filters/__init__.py @@ -23,6 +23,7 @@ from .PlotLineItem import * from .Python import * from .GetColumn import * +from .RenderView import * from .ShaderDemoScene import * from .ShaderFXAA import * from .ShaderIBS import * diff --git a/pycinema/theater/Icons.py b/pycinema/theater/Icons.py index ea874e4c..87d1328f 100644 --- a/pycinema/theater/Icons.py +++ b/pycinema/theater/Icons.py @@ -15,6 +15,7 @@ def update_theme(): Icons.icon_save = '' Icons.icon_slider = '' Icons.icon_list = '' + Icons.icon_directory = '' def toQIcon(svg_str): svg_bytes = bytearray(svg_str, encoding='utf-8') diff --git a/pycinema/theater/Theater.py b/pycinema/theater/Theater.py index e351b446..7076d448 100644 --- a/pycinema/theater/Theater.py +++ b/pycinema/theater/Theater.py @@ -17,6 +17,8 @@ class _Theater(QtWidgets.QMainWindow): def __init__(self): super().__init__() + self.setIconSize(QtCore.QSize(512,512)) + # init theme Icons.update_theme(); NodeEditorStyle.update_theme(); @@ -98,13 +100,15 @@ def saveScript(self): for _,filter in pycinema.Filter._filters.items(): script += filter.id + ' = pycinema.filters.' + filter.__class__.__name__+'()\n' + getPortType = lambda p: 'inputs' if p.is_input else 'outputs' + script += '\n# properties\n' for _,filter in pycinema.Filter._filters.items(): for iPortName, iPort in filter.inputs.ports(): if iPort.valueIsPort(): - script += filter.id + '.inputs.'+iPortName+ '.set(' + iPort._value.parent.id +'.outputs.'+ iPort._value.name +', False)\n' + script += filter.id + '.inputs.'+iPortName+ '.set(' + iPort._value.parent.id +'.'+getPortType(iPort._value)+'.'+ iPort._value.name +', False)\n' elif iPort.valueIsPortList(): - script += filter.id + '.inputs.'+iPortName+ '.set([' + ','.join([p.parent.id +'.outputs.'+p.name for p in iPort._value]) +'], False)\n' + script += filter.id + '.inputs.'+iPortName+ '.set([' + ','.join([p.parent.id +'.'+getPortType(p._value)+'.'+p.name for p in iPort._value]) +'], False)\n' else: v = iPort.get() if iPort.type == int or iPort.type == float: @@ -225,7 +229,7 @@ def __init__(self, args=[]): Theater.instance.resize(1024, 900) Theater.instance.show() - scriptkey = None + scriptkey = None if len(args)>0 and isinstance(args[0], str): if args[0].endswith('.py'): Theater.instance.loadScript(args[0], scriptkey, args[1:]) @@ -234,7 +238,7 @@ def __init__(self, args=[]): script = pycinema.getPathForScript('browse') Theater.instance.loadScript(script, scriptkey, [args[0]]) - else: + else: script = pycinema.getPathForScript(args[0]) if script: print("loading script: " + script) diff --git a/pycinema/theater/node_editor/Edge.py b/pycinema/theater/node_editor/Edge.py index cab161e9..17523dd0 100644 --- a/pycinema/theater/node_editor/Edge.py +++ b/pycinema/theater/node_editor/Edge.py @@ -2,17 +2,23 @@ from pycinema.theater.node_editor.NodeEditorStyle import NodeEditorStyle as NES -class Edge(QtWidgets.QGraphicsLineItem): +class Edge(QtWidgets.QGraphicsPathItem): def __init__(self,port0,port1,parent=None): super().__init__(parent) + self.port0 = port0 + self.port1 = port1 self.setZValue(NES.Z_EDGE_LAYER) - self.setPen(QtGui.QPen(NES.COLOR_NORMAL, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) - - self.port0 = port0 - self.port1 = port1 + # image_port_names = ['image','images'] + # is_image = port0.port.name in image_port_names or port1.port.name in image_port_names + if self.port0.port.is_input and self.port1.port.is_input: + self.setPen(QtGui.QPen(NES.COLOR_NORMAL, 2, QtCore.Qt.DashLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) + # elif port0.port.name=='table' or port1.port.name=='table': + # self.setPen(QtGui.QPen(NES.COLOR_NORMAL, 2, QtCore.Qt.DotLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) + else: + self.setPen(QtGui.QPen(NES.COLOR_NORMAL, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)) self.port0.parentItem().s_moved.connect(self.update) self.port1.parentItem().s_moved.connect(self.update) @@ -20,7 +26,32 @@ def __init__(self,port0,port1,parent=None): self.update() def update(self): - self.setLine( QtCore.QLineF( - self.port0.mapToItem(self, self.port0.disc.boundingRect().center()), - self.port1.mapToItem(self, self.port1.disc.boundingRect().center()) - )) + p0 = self.port0.mapToItem(self, self.port0.disc.boundingRect().center()) + p1 = self.port1.mapToItem(self, self.port1.disc.boundingRect().center()) + + x0 = p0.x() + y0 = p0.y() + x1 = p1.x() + y1 = p1.y() + path = QtGui.QPainterPath() + path.moveTo(p0) + dx = abs(x0 - x1) + if x00 and len(items1)>0 and items0[0]!=items1[0]: QtNodeEditorView.autoConnect(items0[0],items1[0]) @@ -223,6 +225,7 @@ def computeLayout(force=False): return filters = pycinema.Filter._filters + if len(filters)<1: return if use_pgv: node_string = '' From bdc6afcce8a5250f032cf98d30a5ee502ff555aa Mon Sep 17 00:00:00 2001 From: Jonas Lukasczyk Date: Tue, 13 Aug 2024 10:22:08 +0200 Subject: [PATCH 2/2] test fixes --- pycinema/filters/Shader.py | 18 +++++++++--------- setup.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pycinema/filters/Shader.py b/pycinema/filters/Shader.py index f9328f97..92f99758 100644 --- a/pycinema/filters/Shader.py +++ b/pycinema/filters/Shader.py @@ -102,13 +102,13 @@ def releaseTextures(self): try: Shader.ctx = moderngl.create_standalone_context(require=330) Shader.quad = Shader.ctx.buffer( - numpy.array([ - 1.0, 1.0, - -1.0, 1.0, - -1.0, -1.0, - 1.0, -1.0, - 1.0, 1.0 - ]).astype('f4').tobytes() - ) + numpy.array([ + 1.0, 1.0, + -1.0, 1.0, + -1.0, -1.0, + 1.0, -1.0, + 1.0, 1.0 + ]).astype('f4').tobytes() + ) except: - log.warn("Unable to setup OpenGL context.") + log.warning("Unable to setup OpenGL context.") diff --git a/setup.py b/setup.py index 59b6c59e..12044abb 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ "matplotlib==3.6.0", "py==1.11.0", "Pillow==9.4.0", - "moderngl<6", + "moderngl==5.8.2", "opencv-python==4.7.0.68", "ipycanvas==0.13.1", "ipywidgets==8.0.6",