"""N-panel UI in the 3D viewport, tab "Cthuloid"."""
import bpy
from bpy.types import Panel, UIList
from . import protocol, rig
from .net import SENDER
class CTHULOID_UL_fixtures(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_prop, index):
row = layout.row(align=True)
row.prop(item, "enabled", text="")
row.label(text=item.host or "(no host)", icon='LIGHT_DATA')
text, icon = fixture_state(context.scene, item)
sub = row.row()
sub.alert = icon == 'ERROR'
sub.label(text=text, icon=icon)
def fixture_state(scene, fixture):
"""(words, icon) for one fixture, as the user should read it."""
if not fixture.enabled:
return "off", 'BLANK1'
if not scene.cthuloid.live:
return "not connected", 'RADIOBUT_OFF'
status, note = SENDER.status_of(fixture.host, fixture.port)
if status == "connected" and "held by" in note:
# Shared control: someone else holds a slot ("slot 2 held by alice").
return note, 'LOCKED'
return {
"connected": ("connected", 'RADIOBUT_ON'),
"connecting": ("connecting...", 'SORTTIME'),
"idle": ("connecting...", 'SORTTIME'),
"unreachable": ("unreachable", 'ERROR'),
"lost": ("unreachable", 'ERROR'),
"duplicate": ("same device twice", 'ERROR'),
"error": ("error", 'ERROR'),
}.get(status, (status, 'QUESTION'))
class CTHULOID_MT_tools(bpy.types.Menu):
"""Occasional operations. Everything here is either a one-time migration or
a recovery action -- normal output is automatic while connected."""
bl_label = "Tools"
bl_idname = "CTHULOID_MT_tools"
def draw(self, context):
layout = self.layout
layout.operator("cthuloid.send_now", icon='FILE_REFRESH')
layout.separator()
layout.operator("cthuloid.adopt_scene", icon='IMPORT')
class CthuloidPanelBase:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Cthuloid"
class CTHULOID_PT_main(CthuloidPanelBase, Panel):
bl_label = "Fixtures"
def draw(self, context):
layout = self.layout
props = context.scene.cthuloid
# One switch decides everything. Red when on, like auto-keying, so a
# hot rig is unmistakable; words rather than PLAY/PAUSE icons, which
# collided with the timeline's own transport controls.
row = layout.row(align=True)
row.scale_y = 1.6
row.alert = props.live
row.prop(props, "live", toggle=True,
text="CONNECTED - live (click to disconnect)" if props.live else "CONNECT",
icon='REC' if props.live else 'RADIOBUT_OFF')
row.operator("cthuloid.blackout", text="", icon='CANCEL')
row = layout.row()
row.template_list("CTHULOID_UL_fixtures", "", props, "fixtures", props, "active_index", rows=3)
col = row.column(align=True)
col.operator("cthuloid.add_fixture", text="", icon='ADD')
col.operator("cthuloid.remove_fixture", text="", icon='REMOVE')
col.separator()
col.menu("CTHULOID_MT_tools", text="", icon='DOWNARROW_HLT')
if props.live:
layout.label(text="Edits and playback go straight to the lights")
else:
layout.label(text="Not connected: nothing is sent to the lights")
class CTHULOID_PT_fixture(CthuloidPanelBase, Panel):
bl_label = "Fixture"
bl_parent_id = "CTHULOID_PT_main"
@classmethod
def poll(cls, context):
props = context.scene.cthuloid
return 0 <= props.active_index < len(props.fixtures)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.scene.cthuloid
fixture = props.fixtures[props.active_index]
col = layout.column(align=True)
col.prop(fixture, "host")
col.prop(fixture, "port")
text, icon = fixture_state(context.scene, fixture)
box = layout.box()
box.alert = icon == 'ERROR'
box.label(text=text, icon=icon)
_status, err = SENDER.status_of(fixture.host, fixture.port)
if err and props.live and fixture.enabled:
box.label(text=err)
layout.separator()
col = layout.column(align=True)
col.prop(fixture, "red_max_pwm")
if fixture.red_max_pwm > protocol.FIRMWARE_RED_MAX_PWM:
col.label(text="Above firmware limit; command will be rejected", icon='ERROR')
col = layout.column(align=True)
col.prop(fixture, "use_energy")
sub = col.column()
sub.enabled = fixture.use_energy
sub.prop(fixture, "energy_ref")
layout.separator()
col = layout.column(align=True)
col.prop(fixture, "fade_mode")
sub = col.column()
sub.enabled = fixture.fade_mode == 'FIXED'
sub.prop(fixture, "fade_ms")
class CTHULOID_PT_hierarchy(CthuloidPanelBase, Panel):
bl_label = "Hierarchy"
bl_parent_id = "CTHULOID_PT_main"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
props = context.scene.cthuloid
return 0 <= props.active_index < len(props.fixtures)
def draw(self, context):
layout = self.layout
scene = context.scene
props = scene.cthuloid
fixture = props.fixtures[props.active_index]
lights = rig.fixture_lights(scene, fixture)
col = layout.column(align=True)
for slot in range(1, protocol.SLOTS_PER_FIXTURE + 1):
obj = lights.get(slot)
row = col.row(align=True)
row.label(text="Slot %d" % slot)
if obj is None:
row.label(text="empty -> 0 0 0", icon='RADIOBUT_OFF')
else:
row.label(text=obj.name, icon='LIGHT_DATA')
for slot, a, b in rig.slot_conflicts(scene, fixture):
layout.label(text="Slot %d claimed by %s and %s" % (slot, a, b), icon='ERROR')
layout.separator()
col = layout.column(align=True)
col.operator("cthuloid.rebuild_rig", icon='FILE_REFRESH')
col.operator("cthuloid.assign_light", icon='LINKED')
col.operator("cthuloid.sync_names", icon='SORTALPHA')
class CTHULOID_PT_firmware(CthuloidPanelBase, Panel):
bl_label = "Firmware Settings"
bl_parent_id = "CTHULOID_PT_main"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
props = context.scene.cthuloid
return 0 <= props.active_index < len(props.fixtures)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.scene.cthuloid
fixture = props.fixtures[props.active_index]
col = layout.column(align=True)
col.prop(fixture, "gamma_b")
col.prop(fixture, "gamma_g")
col.prop(fixture, "gamma_r")
layout.prop(fixture, "freq")
layout.operator("cthuloid.push_settings", icon='EXPORT')
class CTHULOID_PT_output(CthuloidPanelBase, Panel):
bl_label = "Output"
bl_parent_id = "CTHULOID_PT_main"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
props = context.scene.cthuloid
col = layout.column(align=True)
col.prop(props, "send_on_frame")
col.prop(props, "send_on_edit")
col.prop(props, "max_hz")
class CTHULOID_PT_light(CthuloidPanelBase, Panel):
bl_label = "Cthuloid Light"
bl_context = "objectmode"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.object is not None and context.object.type == 'LIGHT'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
obj = context.object
lp = obj.cthuloid_light
layout.prop(lp, "is_fixture_light")
col = layout.column(align=True)
col.enabled = lp.is_fixture_light
col.prop(lp, "slot")
fixture = None
for f in context.scene.cthuloid.fixtures:
if f.id == lp.fixture_id:
fixture = f
break
col.label(text="Fixture: %s" % (fixture.host if fixture else "unassigned"))
CLASSES = (
CTHULOID_UL_fixtures,
CTHULOID_MT_tools,
CTHULOID_PT_main,
CTHULOID_PT_fixture,
CTHULOID_PT_hierarchy,
CTHULOID_PT_firmware,
CTHULOID_PT_output,
CTHULOID_PT_light,
)
# Fixture status changes on the worker threads, which cannot redraw a panel,
# so without this the words above only update when the mouse moves over them.
_REDRAW_INTERVAL = 0.5
def _redraw_tick():
wm = bpy.context.window_manager
if wm is not None and SENDER.has_targets():
for window in wm.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
return _REDRAW_INTERVAL
def register():
for cls in CLASSES:
bpy.utils.register_class(cls)
bpy.app.timers.register(_redraw_tick, first_interval=_REDRAW_INTERVAL, persistent=True)
def unregister():
if bpy.app.timers.is_registered(_redraw_tick):
bpy.app.timers.unregister(_redraw_tick)
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)