diff --git a/data.py b/data.py index 8a30c67..55dfe96 100644 --- a/data.py +++ b/data.py @@ -104,7 +104,7 @@ def set(universe, addr, val): return if DMX_Data._dmx is not None: - dmx = bpy.context.scene.dmx # hmm, here we use non cached dmx, probably this was safer... + dmx = bpy.context.scene.dmx # hmm, here we use non cached dmx, probably this was safer... if dmx.get_selected_live_dmx_universe().input == "BLENDERDMX": dmx = bpy.context.scene.dmx dmx.dmx_values[addr - 1].channel = val @@ -113,7 +113,7 @@ def set(universe, addr, val): # LiveDMX view if DMX_Data._dmx is not None: - dmx = bpy.context.scene.dmx # ...or maybe it prevents using this call before the class is ready? + dmx = bpy.context.scene.dmx # ...or maybe it prevents using this call before the class is ready? selected_live_dmx_universe = dmx.get_selected_live_dmx_universe() if selected_live_dmx_universe is None: # this should not happen raise ValueError("Missing selected universe, as if DMX base class is empty...") @@ -121,14 +121,17 @@ def set(universe, addr, val): DMX_Data._live_view_data = DMX_Data._universes[universe] @staticmethod - def set_virtual(fixture, attribute, value): + def set_virtual(fixture, attribute, geometry, value): """Set value of virtual channel for given fixture""" DMX_Log.log.debug((fixture, attribute, value)) if value > 255: return if fixture not in DMX_Data._virtuals: DMX_Data._virtuals[fixture] = {} - DMX_Data._virtuals[fixture][attribute] = value + if attribute not in DMX_Data._virtuals[fixture]: + DMX_Data._virtuals[fixture][attribute] = {} + DMX_Data._virtuals[fixture][attribute]["value"] = value + DMX_Data._virtuals[fixture][attribute]["geometry"] = geometry @staticmethod def get_virtual(fixture): diff --git a/dmx.py b/dmx.py index edb2a7d..4e671dc 100644 --- a/dmx.py +++ b/dmx.py @@ -59,6 +59,7 @@ from .panels import profiles as Profiles from .panels import distribute as distribute from .panels import classing as classing +from .panels import subfixtures as subfixtures from .preferences import DMX_Preferences, DMX_Regenrate_UUID from .group import FixtureGroup, DMX_Group @@ -117,7 +118,8 @@ class DMX(PropertyGroup): DMX_MVR_Xchange_Client, DMX_MVR_Xchange, DMX_Regenrate_UUID, - DMX_Preferences) + DMX_Preferences, + subfixtures.DMX_Subfixture) # Classes to be registered # The registration is done in two steps. The second only runs @@ -167,6 +169,9 @@ class DMX(PropertyGroup): groups.DMX_PT_Groups, classing.DMX_UL_Class, classing.DMX_PT_Classes, + subfixtures.DMX_PT_Subfixtures, + subfixtures.DMX_UL_Subfixture, + subfixtures.DMX_OT_Subfixture_Clear, programmer.DMX_OT_Programmer_DeselectAll, programmer.DMX_OT_Programmer_SelectAll, programmer.DMX_OT_Programmer_SelectFiltered, diff --git a/dmx_temp_data.py b/dmx_temp_data.py index 61b6c67..c342efd 100644 --- a/dmx_temp_data.py +++ b/dmx_temp_data.py @@ -19,6 +19,7 @@ from . import fixture as fixture from .logging import DMX_Log +from .panels import subfixtures from .mvr_xchange import DMX_MVR_Xchange from .panels import profiles as Profiles @@ -31,6 +32,7 @@ CollectionProperty) from bpy.types import (PropertyGroup) +from .fixture import DMX_Fixture_Channel from .i18n import DMX_Lang _ = DMX_Lang._ @@ -129,3 +131,20 @@ def onUpdateLoggingFilter(self, context): dist_gap: FloatProperty(name="Gap", default=2) dist_diameter: FloatProperty(name="Diameter", default=2) dist_rotate: BoolProperty(name="Rotate", default=False) + programmer_source: StringProperty(name="Source") + + subfixtures: CollectionProperty( + name = _("Subfixtures"), + type=subfixtures.DMX_Subfixture + ) + + active_subfixtures: CollectionProperty( + name = _("Active Subfixtures"), + type=PropertyGroup + ) + + active_subfixture_i : IntProperty( + default = -1 + ) + + selected_fixture_label: StringProperty() diff --git a/fixture.py b/fixture.py index 71ddd64..68501f0 100644 --- a/fixture.py +++ b/fixture.py @@ -544,17 +544,32 @@ def build(self, name, profile, mode, universe, address, gel_color, display_beams # Interface Methods # def setDMX(self, pvalues): - channels = [c.id for c in self.channels] - virtuals = [c.id for c in self.virtual_channels] + + temp_data = bpy.context.window_manager.dmx + + #channels = [c.id for c in self.channels] + #virtuals = [c.id for c in self.virtual_channels] + for attribute, value in pvalues.items(): - for idx, channel in enumerate(channels): - if channel == attribute: - DMX_Log.log.info(("Set DMX data", channel, value)) - DMX_Data.set(self.universe, self.address+idx, value) - for vchannel in virtuals: - if vchannel == attribute: - DMX_Log.log.info(("Set Virtual data", attribute, value)) - DMX_Data.set_virtual(self.name, attribute, value) + for idx, channel in enumerate(self.channels): + if channel.id == attribute: + if len(temp_data.active_subfixtures)>0: + if any(channel.geometry == g.name for g in temp_data.active_subfixtures): + DMX_Log.log.info(("Set DMX data", channel.id, value)) + DMX_Data.set(self.universe, self.address+idx, value) + else: + DMX_Log.log.info(("Set DMX data", channel, value)) + DMX_Data.set(self.universe, self.address+idx, value) + for vchannel in self.virtual_channels: + if vchannel.id == attribute: + if len(temp_data.active_subfixtures)>0: + if any(vchannel.geometry == g.name for g in temp_data.active_subfixtures): + DMX_Log.log.info(("Set Virtual data", attribute, value)) + geometry = next(vchannel.geometry == g.name for g in temp_data.active_subfixtures) + DMX_Data.set_virtual(self.name, attribute, geometry, value) + else: + DMX_Log.log.info(("Set Virtual data", attribute, value)) + DMX_Data.set_virtual(self.name, attribute, None, value) def render(self, skip_cache = False, current_frame = None): @@ -563,12 +578,12 @@ def render(self, skip_cache = False, current_frame = None): return channels = [c.id for c in self.channels] - virtual_channels = [c.id for c in self.virtual_channels] + #virtual_channels = [c.id for c in self.virtual_channels] data = DMX_Data.get(self.universe, self.address, len(channels)) data_virtual = DMX_Data.get_virtual(self.name) - s_data = [int(b) for b in data] + [int(b) for b in data_virtual.values()] # create cache + s_data = [int(b) for b in data] + [int(b["value"]) for b in data_virtual.values()] # create cache if list(self["dmx_values"]) == s_data: # this helps to eliminate flicker with Ethernet DMX signal when the data for this particular device is not changing if skip_cache is False: # allow to save a keyframe when using the programmer in Blender DMX_Log.log.debug("caching DMX") @@ -591,8 +606,8 @@ def render(self, skip_cache = False, current_frame = None): tilt_rotating_geometries={} gobo1 = [None, None] #gobo selection (Gobo1, Gobo2), gobo indexing/rotation (Gobo1Pos, Gobo2Pos) - for attribute in virtual_channels: - geometry = None # for now. But, no way to know, as BlenderDMX controls are universal + for vchannel in self.virtual_channels: + geometry = vchannel.geometry # for now. But, no way to know, as BlenderDMX controls are universal if geometry not in rgb_mixing_geometries.keys(): rgb_mixing_geometries[geometry] = [None] * 12 # R, G, B, White, WW, CW, Amber, Lime, UV, cyan, magenta, yellow if geometry not in xyz_moving_geometries.keys(): @@ -605,44 +620,44 @@ def render(self, skip_cache = False, current_frame = None): pan_rotating_geometries[geometry]=[None, 1] if geometry not in tilt_rotating_geometries.keys(): tilt_rotating_geometries[geometry]=[None, 1] - if attribute in data_virtual: - if attribute == "Shutter1": shutter_dimmer_geometries[geometry][0] = data_virtual[attribute] - elif attribute == "Dimmer": shutter_dimmer_geometries[geometry][1] = data_virtual[attribute] - elif attribute == "+Dimmer": - shutter_dimmer_geometries[geometry][1] = shutter_dimmer_geometries[geometry][1] * 256 + data_virtual[attribute] + if vchannel.id in data_virtual: + if vchannel.id == "Shutter1": shutter_dimmer_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "Dimmer": shutter_dimmer_geometries[geometry][1] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "+Dimmer": + shutter_dimmer_geometries[geometry][1] = shutter_dimmer_geometries[geometry][1] * 256 + data_virtual[vchannel.id]["value"] shutter_dimmer_geometries[geometry][3] = 256 - elif (attribute == "ColorAdd_R" or attribute == "ColorRGB_Red"): rgb_mixing_geometries[geometry][0] = data_virtual[attribute] - elif (attribute == "ColorAdd_G" or attribute == "ColorRGB_Green"): rgb_mixing_geometries[geometry][1] = data_virtual[attribute] - elif (attribute == "ColorAdd_B" or attribute == "ColorRGB_Blue"): rgb_mixing_geometries[geometry][2] = data_virtual[attribute] - elif attribute == "ColorSub_C": cmy[0] = data_virtual[attribute] - elif attribute == "ColorSub_M": cmy[1] = data_virtual[attribute] - elif attribute == "ColorSub_Y": cmy[2] = data_virtual[attribute] - elif attribute == "Pan": - panTilt[0] = data_virtual[attribute] - pan_rotating_geometries[geometry][0] = data_virtual[attribute] - elif attribute == "+Pan": - panTilt[0] = panTilt[0] * 256 + data_virtual[attribute] + elif (vchannel.id == "ColorAdd_R" or vchannel.id == "ColorRGB_Red"): rgb_mixing_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif (vchannel.id == "ColorAdd_G" or vchannel.id == "ColorRGB_Green"): rgb_mixing_geometries[geometry][1] = data_virtual[vchannel.id]["value"] + elif (vchannel.id == "ColorAdd_B" or vchannel.id == "ColorRGB_Blue"): rgb_mixing_geometries[geometry][2] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "ColorSub_C": cmy[0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "ColorSub_M": cmy[1] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "ColorSub_Y": cmy[2] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "Pan": + panTilt[0] = data_virtual[vchannel.id]["value"] + pan_rotating_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "+Pan": + panTilt[0] = panTilt[0] * 256 + data_virtual[vchannel.id]["value"] panTilt[2] = 256 # 16bit - pan_rotating_geometries[geometry][0] = pan_rotating_geometries[geometry][0] * 256 + data_virtual[attribute] + pan_rotating_geometries[geometry][0] = pan_rotating_geometries[geometry][0] * 256 + data_virtual[vchannel.id]["value"] pan_rotating_geometries[geometry][2] = 256 - elif attribute == "Tilt": - panTilt[1] = data_virtual[attribute] - tilt_rotating_geometries[geometry][0] = data_virtual[attribute] - elif attribute == "+Tilt": - panTilt[1] = panTilt[1] * 256 + data_virtual[attribute] + elif vchannel.id == "Tilt": + panTilt[1] = data_virtual[vchannel.id]["value"] + tilt_rotating_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "+Tilt": + panTilt[1] = panTilt[1] * 256 + data_virtual[vchannel.id]["value"] panTilt[3] = 256 # 16bit - tilt_rotating_geometries[geometry][0] = tilt_rotating_geometries[geometry][0] * 256 + data_virtual[attribute] + tilt_rotating_geometries[geometry][0] = tilt_rotating_geometries[geometry][0] * 256 + data_virtual[vchannel.id]["value"] tilt_rotating_geometries[geometry][2] = 256 - elif attribute == "Zoom": zoom = data_virtual[attribute] - elif attribute == "XYZ_X": xyz_moving_geometries[geometry][0] = data_virtual[attribute] - elif attribute == "XYZ_Y": xyz_moving_geometries[geometry][1] = data_virtual[attribute] - elif attribute == "XYZ_Z": xyz_moving_geometries[geometry][2] = data_virtual[attribute] - elif attribute == "Rot_X": xyz_rotating_geometries[geometry][0] = data_virtual[attribute] - elif attribute == "Rot_Y": xyz_rotating_geometries[geometry][1] = data_virtual[attribute] - elif attribute == "Rot_Z": xyz_rotating_geometries[geometry][2] = data_virtual[attribute] + elif vchannel.id == "Zoom": zoom = data_virtual[vchannel.id]["value"] + elif vchannel.id == "XYZ_X": xyz_moving_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "XYZ_Y": xyz_moving_geometries[geometry][1] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "XYZ_Z": xyz_moving_geometries[geometry][2] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "Rot_X": xyz_rotating_geometries[geometry][0] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "Rot_Y": xyz_rotating_geometries[geometry][1] = data_virtual[vchannel.id]["value"] + elif vchannel.id == "Rot_Z": xyz_rotating_geometries[geometry][2] = data_virtual[vchannel.id]["value"] for c in range(len(channels)): geometry=self.channels[c].geometry @@ -716,6 +731,8 @@ def render(self, skip_cache = False, current_frame = None): self.remove_unset_geometries_from_multigeometry_attributes_1(pan_rotating_geometries) self.remove_unset_geometries_from_multigeometry_attributes_1(tilt_rotating_geometries) + + colorwheel_color = None if (color1 is not None): colorwheel_color = self.get_colorwheel_color(color1) @@ -1384,14 +1401,19 @@ def hide_gobo(self, hide = True, current_frame = None): def has_attribute(self, attribute, lower = False): + temp_data = bpy.context.window_manager.dmx def low(id): if lower: return id.lower() else: return id + if len(temp_data.active_subfixtures) > 0: + real = any([attribute in low(channel.id) for channel in self.channels if any(channel.geometry == g.name for g in temp_data.active_subfixtures)]) + virtual = any([attribute in channel.id for channel in self.virtual_channels if any(channel.geometry == g.name for g in temp_data.active_subfixtures)]) + else: + real = any([attribute in low(channel.id) for channel in self.channels]) + virtual = any([attribute in channel.id for channel in self.virtual_channels]) - real = any([attribute in low(channel.id) for channel in self.channels]) - virtual = any([attribute in channel.id for channel in self.virtual_channels]) return (real or virtual) diff --git a/panels/programmer.py b/panels/programmer.py index 2ea6433..f5ee4ba 100644 --- a/panels/programmer.py +++ b/panels/programmer.py @@ -379,10 +379,12 @@ class DMX_PT_Programmer(Panel): bl_category = "DMX" bl_context = "objectmode" + def draw(self, context): layout = self.layout scene = context.scene dmx = scene.dmx + temp_data = bpy.context.window_manager.dmx locked = False selected_fixtures = [] @@ -398,10 +400,12 @@ def draw(self, context): selected = len(selected_fixtures) > 0 selected_fixture_label = "" + selected_fixture_class = None if len(selected_fixtures) == 0: selected_fixture_label = _("Nothing selected") elif len(selected_fixtures) == 1: selected_fixture_label = selected_fixtures[0].name + selected_fixture_class = selected_fixtures[0] else: profiles=[] for sel_fixture in selected_fixtures: @@ -414,9 +418,14 @@ def draw(self, context): profiles = list(set(profiles)) if len(profiles)==1: selected_fixture_label = f"{len(selected_fixtures)} {profiles[0]}" + selected_fixture_class = selected_fixtures[0] else: selected_fixture_label = _("{} selected").format(len(selected_fixtures)) + if temp_data.selected_fixture_label != selected_fixture_label: + temp_data.subfixtures.clear() + temp_data.selected_fixture_label = selected_fixture_label + row = layout.row() row.operator("dmx.select_all", text="", icon="SELECT_EXTEND") row.operator("dmx.select_invert", text="", icon="SELECT_SUBTRACT") @@ -449,6 +458,7 @@ def draw(self, context): row.operator("dmx.toggle_camera", text=_("Camera")) row.enabled = selected + row=layout.row() box = layout.column().box() box.template_color_picker(scene.dmx, "programmer_color", value_slider=True) row = box.row() @@ -456,13 +466,37 @@ def draw(self, context): col2 = row.column() col1.prop(scene.dmx, "programmer_color") col2.operator("dmx.reset_color", icon="TRASH", text="") - box.prop(scene.dmx, "programmer_dimmer", text=_("Dimmer"), translate=False) + box.prop(scene.dmx, "programmer_dimmer", text=_("Dimmer"), translate=False, slider = True) + + + if selected_fixture_class is not None: + all_channels=[] + for channel in selected_fixture_class.channels: + if channel.geometry not in all_channels: + all_channels.append(channel.geometry) + + for channel in selected_fixture_class.virtual_channels: + if channel.geometry not in all_channels: + all_channels.append(channel.geometry) + + if len(temp_data.subfixtures) < 1: + tmp_active = [x.name for x in temp_data.active_subfixtures] + temp_data.active_subfixtures.clear() + for channel in all_channels: + sub = temp_data.subfixtures.add() + sub.name = channel + if channel in tmp_active: + sub.enabled = True + if channel not in temp_data.active_subfixtures: + temp_data.active_subfixtures.add().name = channel + - if len(selected_fixtures) == 1: if selected_fixtures[0].has_attribute("Pan"): row = box.row() col1 = row.column() - col1.prop(scene.dmx, "programmer_pan", text=_("Pan"), translate=False) + col1.prop(scene.dmx, "programmer_pan", text=_("Pan"), translate=False, slider = True) + + if selected_fixtures[0].ignore_movement_dmx == True: col2 = row.column() col2.operator("dmx.ignore_movement_false", text="", icon="UNLOCKED") @@ -470,13 +504,13 @@ def draw(self, context): if selected_fixtures[0].has_attribute("Tilt"): row = box.row() col1 = row.column() - col1.prop(scene.dmx, "programmer_tilt", text=_("Tilt"), translate=False) + col1.prop(scene.dmx, "programmer_tilt", text=_("Tilt"), translate=False, slider = True) if selected_fixtures[0].ignore_movement_dmx == True: col2 = row.column() col2.operator("dmx.ignore_movement_false", text="", icon="UNLOCKED") col1.enabled = False if selected_fixtures[0].has_attribute("Zoom"): - box.prop(scene.dmx, "programmer_zoom", text=_("Zoom"), translate=False) + box.prop(scene.dmx, "programmer_zoom", text=_("Zoom"), translate=False, slider = True) if selected_fixtures[0].has_attribute("Color1") or selected_fixtures[0].has_attribute("Color2") or selected_fixtures[0].has_attribute("ColorMacro1"): box.prop(scene.dmx, "programmer_color_wheel", text=_("Color Wheel"), translate=False) if selected_fixtures[0].has_attribute("Gobo"): @@ -487,18 +521,18 @@ def draw(self, context): else: row = box.row() col1 = row.column() - col1.prop(scene.dmx, "programmer_pan", text=_("Pan"), translate=False) + col1.prop(scene.dmx, "programmer_pan", text=_("Pan"), translate=False, slider = True) if locked: col2 = row.column() col2.operator("dmx.ignore_movement_false", text="", icon="UNLOCKED") row = box.row() col1 = row.column() - col1.prop(scene.dmx, "programmer_tilt", text=_("Tilt"), translate=False) + col1.prop(scene.dmx, "programmer_tilt", text=_("Tilt"), translate=False, slider = True) if locked: col2 = row.column() col2.operator("dmx.ignore_movement_false", text="", icon="UNLOCKED") - box.prop(scene.dmx, "programmer_zoom", text=_("Zoom"), translate=False) + box.prop(scene.dmx, "programmer_zoom", text=_("Zoom"), translate=False, slider = True) box.prop(scene.dmx, "programmer_color_wheel", text=_("Color Wheel"), translate=False) box.prop(scene.dmx, "programmer_gobo", text=_("Gobo"), translate=False) box.prop(scene.dmx, "programmer_gobo_index", text=_("Gobo Rotation"), translate=False) diff --git a/panels/subfixtures.py b/panels/subfixtures.py new file mode 100644 index 0000000..7d3aadf --- /dev/null +++ b/panels/subfixtures.py @@ -0,0 +1,103 @@ +# Copyright vanous +# +# This file is part of BlenderDMX. +# +# BlenderDMX is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# BlenderDMX is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + +import bpy + +from bpy.types import Panel, Menu, Operator, UIList + +from bpy.props import BoolProperty, StringProperty, CollectionProperty + +from bpy.types import PropertyGroup +from ..i18n import DMX_Lang + +_ = DMX_Lang._ +# List # + + +class DMX_UL_Subfixture(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {"DEFAULT", "COMPACT"}: + col = layout.column() + col.ui_units_x = 3 + col = layout.column() + col.label(text=f"{item.name}") + col = layout.column() + col.prop(item, "enabled", text="") + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text=str(item.id), icon=icon) + + +class DMX_OT_Subfixture_Clear(Operator): + bl_label = _("Clear selection") + bl_idname = "dmx.clear_subfixtures" + bl_description = _("Clear subfixture selection") + bl_options = {"UNDO"} + + def execute(self, context): + temp_data = bpy.context.window_manager.dmx + #temp_data.active_subfixtures.clear() + for sf in temp_data.subfixtures: + sf.enabled=False + return {"FINISHED"} + +# Panel # + + +class DMX_PT_Subfixtures(Panel): + bl_label = "Subfixtures" + bl_idname = "DMX_PT_Subfixtures" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "DMX" + bl_context = "objectmode" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + scene = context.scene + dmx = scene.dmx + temp_data = bpy.context.window_manager.dmx + + layout.template_list("DMX_UL_Subfixture", "", temp_data, "subfixtures", temp_data, "active_subfixture_i", rows=4) + + selected = ",".join([s.name for s in temp_data.active_subfixtures]) + row = layout.row() + col1 = row.column() + if selected: + col1.label(text=f"Selected: {selected}") + col2 = row.column() + col2.operator("dmx.clear_subfixtures", icon="CHECKBOX_DEHLT", text="") + +class DMX_Subfixture(PropertyGroup): + name: StringProperty() + + def onEnable(self, context): + enabled = self.enabled + temp_data = bpy.context.window_manager.dmx + + if enabled: + if self.name not in temp_data.active_subfixtures: + temp_data.active_subfixtures.add().name = self.name + else: + if self.name in temp_data.active_subfixtures: + for index, item in enumerate(temp_data.active_subfixtures): + if item.name == self.name: + temp_data.active_subfixtures.remove(index) + break + + enabled: BoolProperty(update=onEnable, default=False)