OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / blender-addon / cthuloid_lights / ops.py

230 lines · python ref:
"""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)