OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / blender-addon / cthuloid_lights / rig.py

230 lines · python ref:
"""Creating, adopting and maintaining the fixture hierarchy.

Layout produced (matching what quadpanel.py expected, minus the vestigial
armature -- an Empty is the honest container for a light panel; the armature in
single_light1.blend is a leftover from the robot-arm rigs, which are the only
things that ever needed pose bones):

    quad.<host>                 EMPTY, plain axes
      +- bgr-1-<host>           LIGHT
      +- bgr-2-<host>           LIGHT
      +- bgr-3-<host>           LIGHT
      +- bgr-4-<host>           LIGHT
"""

import uuid

import bpy
from mathutils import Vector

from . import protocol

ROOT_PREFIX = "quad."
LIGHT_PREFIX = "bgr-"
SPACING = 0.6


def new_fixture_id():
    return uuid.uuid4().hex[:8]


def light_name(host, slot):
    return "%s%d-%s" % (LIGHT_PREFIX, slot, host)


def root_name(host):
    return "%s%s" % (ROOT_PREFIX, host)


def find_fixture(scene, fixture_id):
    for f in scene.cthuloid.fixtures:
        if f.id == fixture_id:
            return f
    return None


def fixture_lights(scene, fixture):
    """{slot: object} for every light tagged as belonging to this fixture."""
    out = {}
    for obj in scene.objects:
        props = getattr(obj, "cthuloid_light", None)
        if props is None or not props.is_fixture_light:
            continue
        if props.fixture_id != fixture.id:
            continue
        if obj.type != 'LIGHT':
            continue
        # Last one wins on a duplicate slot; report_conflicts() surfaces these.
        out[props.slot] = obj
    return out


def slot_conflicts(scene, fixture):
    seen, dupes = {}, []
    for obj in scene.objects:
        props = getattr(obj, "cthuloid_light", None)
        if props is None or not props.is_fixture_light or props.fixture_id != fixture.id:
            continue
        if props.slot in seen:
            dupes.append((props.slot, seen[props.slot], obj.name))
        else:
            seen[props.slot] = obj.name
    return dupes


def find_root(scene, fixture):
    """The Empty parenting this fixture's lights, if there is one."""
    lights = fixture_lights(scene, fixture)
    for obj in lights.values():
        if obj.parent is not None:
            return obj.parent
    name = root_name(fixture.host)
    return scene.objects.get(name)


# ---------------------------------------------------------------------------
# creation


def _make_light(context, fixture, slot, root):
    name = light_name(fixture.host, slot)
    data = bpy.data.lights.new(name=name, type='POINT')
    data.color = (1.0, 1.0, 1.0)
    data.energy = fixture.energy_ref

    obj = bpy.data.objects.new(name, data)
    context.collection.objects.link(obj)

    obj.parent = root
    obj.location = Vector(((slot - 2.5) * SPACING, 0.0, 0.0))

    props = obj.cthuloid_light
    props.is_fixture_light = True
    props.fixture_id = fixture.id
    props.slot = slot
    return obj


def create_fixture_rig(context, fixture):
    """Create any missing part of the hierarchy. Idempotent -- safe to re-run."""
    scene = context.scene
    created = []

    root = find_root(scene, fixture)
    if root is None:
        root = bpy.data.objects.new(root_name(fixture.host), None)
        root.empty_display_type = 'PLAIN_AXES'
        root.empty_display_size = 0.4
        context.collection.objects.link(root)
        created.append(root.name)

    existing = fixture_lights(scene, fixture)
    for slot in range(1, protocol.SLOTS_PER_FIXTURE + 1):
        if slot in existing:
            obj = existing[slot]
            if obj.parent is None:
                obj.parent = root
            continue
        created.append(_make_light(context, fixture, slot, root).name)

    return root, created


def sync_names(scene, fixture):
    """Rename root and lights to match the fixture host. Cosmetic only."""
    renamed = 0
    root = find_root(scene, fixture)
    if root is not None:
        want = root_name(fixture.host)
        if root.name != want:
            root.name = want
            renamed += 1
    for slot, obj in fixture_lights(scene, fixture).items():
        want = light_name(fixture.host, slot)
        if obj.name != want:
            obj.name = want
            if obj.data is not None and obj.data.users == 1:
                obj.data.name = want
            renamed += 1
    return renamed


def delete_fixture_rig(scene, fixture):
    """Remove the objects belonging to a fixture. Data-blocks are left to
    Blender's normal orphan handling rather than force-purged."""
    removed = 0
    root = find_root(scene, fixture)
    for obj in list(fixture_lights(scene, fixture).values()):
        bpy.data.objects.remove(obj, do_unlink=True)
        removed += 1
    if root is not None and not root.children:
        bpy.data.objects.remove(root, do_unlink=True)
        removed += 1
    return removed


# ---------------------------------------------------------------------------
# adoption of legacy name-encoded rigs


def parse_legacy_name(name):
    """"bgr-1-192.168.50.138" -> (1, "192.168.50.138").

    maxsplit=2 matters: a hostname may itself contain hyphens ("show-light01"),
    which the original str.split('-') mangled into the wrong host.
    """
    if not name.startswith(LIGHT_PREFIX):
        return None
    parts = name.split("-", 2)
    if len(parts) != 3:
        return None
    try:
        slot = int(parts[1])
    except ValueError:
        return None
    host = parts[2].strip()
    if not host or slot < 1 or slot > protocol.SLOTS_PER_FIXTURE:
        return None
    return slot, host


def adopt_scene(context):
    """Import a legacy name-encoded rig into the property-based model.

    Objects keep their names and parenting; they simply gain the custom
    properties, and a fixture is created per distinct host found.
    """
    scene = context.scene
    by_host = {}

    for obj in scene.objects:
        if obj.type != 'LIGHT':
            continue
        parsed = parse_legacy_name(obj.name)
        if parsed is None:
            continue
        slot, host = parsed
        by_host.setdefault(host, []).append((slot, obj))

    adopted_hosts, adopted_lights = [], 0
    for host, entries in by_host.items():
        fixture = None
        for f in scene.cthuloid.fixtures:
            if f.host == host:
                fixture = f
                break
        if fixture is None:
            fixture = scene.cthuloid.fixtures.add()
            fixture.id = new_fixture_id()
            fixture.host = host
            adopted_hosts.append(host)

        for slot, obj in entries:
            props = obj.cthuloid_light
            props.is_fixture_light = True
            props.fixture_id = fixture.id
            props.slot = slot
            adopted_lights += 1

    return adopted_hosts, adopted_lights