"""Operators."""
import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from . import engine, protocol, rig
from .net import SENDER
def _active_fixture(context):
props = context.scene.cthuloid
if 0 <= props.active_index < len(props.fixtures):
return props.fixtures[props.active_index]
return None
class CTHULOID_OT_add_fixture(Operator):
bl_idname = "cthuloid.add_fixture"
bl_label = "Add Fixture"
bl_description = "Create a fixture and build its four-light hierarchy"
bl_options = {'REGISTER', 'UNDO'}
host: StringProperty(name="Host", default="showlight01.local")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
scene = context.scene
host = self.host.strip()
if not host:
self.report({'ERROR'}, "Host must not be empty")
return {'CANCELLED'}
fixture = scene.cthuloid.fixtures.add()
fixture.id = rig.new_fixture_id()
fixture.host = host
scene.cthuloid.active_index = len(scene.cthuloid.fixtures) - 1
root, created = rig.create_fixture_rig(context, fixture)
self.report({'INFO'}, "Fixture %s: created %d object(s)" % (host, len(created)))
return {'FINISHED'}
class CTHULOID_OT_remove_fixture(Operator):
bl_idname = "cthuloid.remove_fixture"
bl_label = "Remove Fixture"
bl_description = "Remove the active fixture. Its objects are kept unless Delete Objects is enabled"
bl_options = {'REGISTER', 'UNDO'}
delete_objects: BoolProperty(name="Delete Objects", default=False)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
scene = context.scene
props = scene.cthuloid
fixture = _active_fixture(context)
if fixture is None:
self.report({'ERROR'}, "No active fixture")
return {'CANCELLED'}
SENDER.forget(fixture.host, fixture.port)
if self.delete_objects:
rig.delete_fixture_rig(scene, fixture)
else:
for obj in rig.fixture_lights(scene, fixture).values():
obj.cthuloid_light.is_fixture_light = False
obj.cthuloid_light.fixture_id = ""
props.fixtures.remove(props.active_index)
props.active_index = max(0, props.active_index - 1)
return {'FINISHED'}
class CTHULOID_OT_rebuild_rig(Operator):
bl_idname = "cthuloid.rebuild_rig"
bl_label = "Rebuild Hierarchy"
bl_description = "Recreate any missing lights or parent for the active fixture"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
fixture = _active_fixture(context)
if fixture is None:
self.report({'ERROR'}, "No active fixture")
return {'CANCELLED'}
root, created = rig.create_fixture_rig(context, fixture)
if created:
self.report({'INFO'}, "Created: %s" % ", ".join(created))
else:
self.report({'INFO'}, "Hierarchy already complete")
return {'FINISHED'}
class CTHULOID_OT_sync_names(Operator):
bl_idname = "cthuloid.sync_names"
bl_label = "Sync Names"
bl_description = "Rename objects to match the fixture host (cosmetic; wiring is held in properties)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
fixture = _active_fixture(context)
if fixture is None:
self.report({'ERROR'}, "No active fixture")
return {'CANCELLED'}
n = rig.sync_names(context.scene, fixture)
self.report({'INFO'}, "Renamed %d object(s)" % n)
return {'FINISHED'}
class CTHULOID_OT_adopt_scene(Operator):
bl_idname = "cthuloid.adopt_scene"
bl_label = "Convert Name-Encoded Lights"
bl_description = (
"One-time migration for older .blend files. Older rigs stored the "
"wiring in the object name (bgr-1-192.168.50.138 meant slot 1 on that "
"host); this converts them to fixtures. Does nothing in a scene built "
"with Add Fixture")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
hosts, lights = rig.adopt_scene(context)
if not lights:
self.report({'WARNING'}, "No legacy 'bgr-<slot>-<host>' lights found")
return {'CANCELLED'}
self.report({'INFO'}, "Adopted %d light(s) into %d new fixture(s)" % (lights, len(hosts)))
return {'FINISHED'}
class CTHULOID_OT_assign_light(Operator):
bl_idname = "cthuloid.assign_light"
bl_label = "Assign Selected"
bl_description = "Assign selected lights to the active fixture, filling free slots"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
fixture = _active_fixture(context)
if fixture is None:
self.report({'ERROR'}, "No active fixture")
return {'CANCELLED'}
taken = set(rig.fixture_lights(context.scene, fixture).keys())
free = [s for s in range(1, protocol.SLOTS_PER_FIXTURE + 1) if s not in taken]
assigned = 0
for obj in context.selected_objects:
if obj.type != 'LIGHT' or not free:
continue
props = obj.cthuloid_light
props.is_fixture_light = True
props.fixture_id = fixture.id
props.slot = free.pop(0)
assigned += 1
if not assigned:
self.report({'WARNING'}, "Nothing assigned (no free slots or no lights selected)")
return {'CANCELLED'}
self.report({'INFO'}, "Assigned %d light(s)" % assigned)
return {'FINISHED'}
class CTHULOID_OT_send_now(Operator):
bl_idname = "cthuloid.send_now"
bl_label = "Resend / Force Refresh"
bl_description = (
"Recovery only -- output is automatic while connected. Re-sends the "
"current scene to every ticked fixture, since an identical payload is "
"otherwise suppressed")
@classmethod
def poll(cls, context):
return context.scene.cthuloid.live
def execute(self, context):
n = engine.push_scene(context.scene, force=True)
self.report({'INFO'}, "Sent to %d fixture(s)" % n)
return {'FINISHED'}
class CTHULOID_OT_push_settings(Operator):
bl_idname = "cthuloid.push_settings"
bl_label = "Push Gamma & Frequency"
bl_description = "Send gamma and PWM frequency to the active fixture (connect first)"
@classmethod
def poll(cls, context):
return context.scene.cthuloid.live
def execute(self, context):
fixture = _active_fixture(context)
if fixture is None:
self.report({'ERROR'}, "No active fixture")
return {'CANCELLED'}
engine.push_settings(context.scene, fixture)
self.report({'INFO'}, "Pushed settings to %s" % fixture.host)
return {'FINISHED'}
class CTHULOID_OT_blackout(Operator):
bl_idname = "cthuloid.blackout"
bl_label = "Blackout"
bl_description = "Stop any fade and drive every channel of every fixture to zero"
def execute(self, context):
n = engine.blackout(context.scene)
self.report({'INFO'}, "Blacked out %d fixture(s)" % n)
return {'FINISHED'}
CLASSES = (
CTHULOID_OT_add_fixture,
CTHULOID_OT_remove_fixture,
CTHULOID_OT_rebuild_rig,
CTHULOID_OT_sync_names,
CTHULOID_OT_adopt_scene,
CTHULOID_OT_assign_light,
CTHULOID_OT_send_now,
CTHULOID_OT_push_settings,
CTHULOID_OT_blackout,
)
def register():
for cls in CLASSES:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)