"""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