commit 659d9dccbed97357f8cbdecdd056231cc3672354 Author: Jannik Vogel Date: Tue Feb 11 21:48:13 2025 +0100 Initial snapshot diff --git a/LeitLightLightLeitLeidLite/__init__.py b/LeitLightLightLeitLeidLite/__init__.py new file mode 100644 index 0000000..a4a9fa3 --- /dev/null +++ b/LeitLightLightLeitLeidLite/__init__.py @@ -0,0 +1,326 @@ +import json +import math + +# This is the source information: +# - x/y is in LED distances +# - x is distance from right wall +# - y is distance from ceiling near door +# - Indices increase along rows +# - Cells are [right, ..., left] +# - Index increases from right-to-left if rev else left-to-right +# - Negative numbers in cells (also fractions) denote a gap +rows = [ + + # Controller: 0-218 + + # 139, # unter decke (rechts) + { 'y': 0/2, 'x': 0, 'cells': [58, 60, 21], 'rev': False, 'name': 'unter_decke_rechts' }, + + # 80, # unter decke (links) + { 'y': -5/2, 'x': 139, 'cells': [19, 39, 22], 'rev': False, 'name': 'unter_decke_links' }, + + # Controller: 219-702 + + #236, # ueber ampel + { 'y': 40/2, 'x': 0, 'cells': [57, 59, 39, 37, 44], 'rev': False, 'name': 'ueber_ampel' }, + + #-248, # unter ampel + { 'y': 70/2, 'x': 0, 'cells': [57, 59, 40, 37, 55], 'rev': True, 'name': 'unter_ampel' }, + + # Controller: 703-1295 + + #153, # ueber garderobe + { 'y': 100/2, 'x': 0, 'cells': [55, 58, 40], 'rev': False, 'name': 'ueber_garderobe' }, + + #35, # gewuerze (oben) + { 'y': 115/2, 'x': 153, 'cells': [35], 'rev': False, 'name': 'gewuerze_oben' }, + + #47, # unter schrank + { 'y': 110/2, 'x': 153+35, 'cells': [47], 'rev': False, 'name': 'unter_schrank' }, + + #-36, # gewuerze (unten) + { 'y': 130/2, 'x': 153, 'cells': [36], 'rev': True, 'name': 'gewuerze_unten' }, + + #-89, # zwei von unten #FIXME: Bad source? + { 'y': 130/2, 'x': 55, 'cells': [49+5, 40], 'rev': True, 'name': 'zwei_von_unten' }, + + # 100, # eins von unten #FIXME: Bad source? + { 'y': 160/2, 'x': 55, 'cells': [61-5, 39], 'rev': False, 'name': 'eins_von_unten' }, + + #FIXME: Missing one LED? + # -132 # unten + { 'y': 190/2, 'x': 55, 'cells': [57, 39, 36], 'rev': True, 'name': 'unten' }, +] + + + +# 3D vector math + +def scaleV(v, s): + return (v[0]*s,v[1]*s,v[2]*s) + +def addV(a, b): + return (a[0]+b[0], a[1]+b[1], a[2]+b[2]) + +def subV(a, b): + return addV(a, scaleV(b, -1.0)) + +def mulV(a, b): + return (a[0]*b[0], a[1]*b[1], a[2]*b[2]) + +def dotV(a, b): + t = mulV(a, b) + return t[0]+t[1]+t[2] + +def normalizeV(v): + l = math.sqrt(dotV(v, v)) + return scaleV(v, 1.0 / l) + +def lerp(a, b, p): + return round(a + (b - a) * p) + +def lerpV(a, b, p): + return [lerp(a[0],b[0],p), lerp(a[1],b[1],p), lerp(a[2],b[2],p)] + + + +# Strip functions + +# Turn strips into 3D line segments +def getSourceStrips(rows): + sourceStrips = [] + index = 0 + for row in rows: + + rowSourceStrips = [] + + # Extract cells (right to left) + step = (1, 0, 0) + start = (row['x'], row['y'], 0) + for length in row['cells']: + end = addV(start, scaleV(step, abs(length))) + if length > 0: + rowSourceStrip = { + 'start': start, + 'end': end, + 'count': length, + 'index': len(rowSourceStrips), + 'name': row['name'] + } + rowSourceStrips += [rowSourceStrip] + start = end + + if row['rev']: + rowSourceStrips = rowSourceStrips[::-1] + for rowSourceStrip in rowSourceStrips: + rowSourceStrip['start'], rowSourceStrip['end'] = rowSourceStrip['end'], rowSourceStrip['start'] + + sourceStrips += rowSourceStrips + + print(sourceStrips) + return sourceStrips + +# Adds index information to each strip +def bakeStrips(sourceStrips): + strips = [] + first = 0 + for sourceStrip in sourceStrips: + strip = dict(sourceStrip) + + # Fix stupidity in sourceStrip + strip['direction'] = scaleV(subV(sourceStrip['end'], sourceStrip['start']), 1.0 / sourceStrip['count']) + del strip['end'] + + # Bake the index + strip['first'] = first + strips += [strip] + + # Prepare for next strip + first += sourceStrip['count'] + + return strips + +# Only keeps strip in range start to end (in range 0.0 - 1.0) +def extractStrip(strip, start, startInclusive, end, endInclusive): + strip = dict(strip) + count = strip['count'] + halfStep = 1.0 / count + startRounder = math.floor if startInclusive else math.ceil + endRounder = math.ceil if endInclusive else math.floor + startIndex = max(0, min(startRounder(start * (count - 1) + halfStep), count - 1)) + endIndex = max(0, min(endRounder(end * (count - 1) - halfStep), count - 1)) + strip['first'] += startIndex + strip['start'] = addV(strip['start'], scaleV(strip['direction'], startIndex)) + strip['count'] = (endIndex - startIndex) + 1 + assert(strip['count'] >= 0) + return strip + +# Only keep portions of the clip above the plane surface +def clipStripAgainstPlane(strip, origin, direction): + strip = dict(strip) + start = strip['start'] + stripDirection = strip['direction'] + count = strip['count'] + + # If we don't have any LEDs left, then there's nothing to clip against + if count == 0: + return strip + + assert(count > 0) + + startToEnd = scaleV(stripDirection, count - 1) + startToOrigin = subV(origin, start) + towardsPlane = dotV(startToEnd, direction) # 0 if in plane, positive if along plane direction + abovePlane = dotV(startToOrigin, direction) # negative if start above + + isStartAbove = abovePlane < 0.0 + isTowardsPlane = (towardsPlane < 0.0) == isStartAbove + + # Strip parallel to plane + if (abs(towardsPlane) <= 1.0e-6): + + # Force no intersection + t = -1.0 + + # Any other case + else: + t = abovePlane / towardsPlane + + print("above" if isStartAbove else "behind", "towards" if isTowardsPlane else "away", t) + + # No intersection + if t <= 0 or t >= 1: + # Remove everything if we are below the plane + if not isStartAbove: + strip['count'] = 0 + return strip + + # Handle intersection + if isStartAbove: + if isTowardsPlane: + # Remove end of strip + strip = extractStrip(strip, 0.0, True, t, True) + else: + # Keep entire strip + pass + else: + if isTowardsPlane: + # Remove start of strip + strip = extractStrip(strip, t, True, 1.0, True) + else: + # Remove entire strip + strip['count'] = 0 + return strip + +def clipStripsAgainstPlane(strips, origin, direction): + clippedStrips = [] + for strip in strips: + clippedStrip = clipStripAgainstPlane(strip, origin, direction) + clippedStrips += [clippedStrip] + return clippedStrips + +def clipStripsAgainstAabb(strips, xyz0, xyz1): + strips = clipStripsAgainstPlane(strips, xyz0, ( 1, 0, 0)) + strips = clipStripsAgainstPlane(strips, xyz1, (-1, 0, 0)) + strips = clipStripsAgainstPlane(strips, xyz0, ( 0, 1, 0)) + strips = clipStripsAgainstPlane(strips, xyz1, ( 0, -1, 0)) + strips = clipStripsAgainstPlane(strips, xyz0, ( 0, 0, 1)) + strips = clipStripsAgainstPlane(strips, xyz1, ( 0, 0, -1)) + return strips + +# Debug helpers + +def generateObj(strips, onlyBounds=False): + s = [] + vs = [] + ls = [] + for strip in strips: + count = strip['count'] + assert(count >= 0) + l = [] + if onlyBounds: + for i in range(2): + v = addV(strip['start'], scaleV(strip['direction'], i * (count - 1))) + vs += [v] + l += [len(vs)] + else: + for i in range(count): + v = addV(strip['start'], scaleV(strip['direction'], i)) + vs += [v] + l += [len(vs)] + ls += [(l, strip['name'], strip['index'])] + for v in vs: + s += ["v %f %f %f" % (v[0], v[1], v[2])] + for l in ls: + s += ["# %s [%d]" % (l[1], l[2])] + if len(l[0]) >= 2: + s += ["l " + " ".join(["%d" % i for i in l[0]])] + return "\n".join(s) + +def testStrip(a, b, count=10): + return { + 'name': 'test', + 'index': 0, + 'start': a, + 'direction': scaleV(subV(b, a), 1.0 / (count - 1)), + 'first': 0, + 'count': count + } + +def debugStrip(strip): + strip = dict(strip) + strip['end'] = addV(strip['start'], scaleV(strip['direction'], strip['count'] - 1)) + #del strip['direction'] + return strip + + +def shadeStrips(strips, shader): + stripDatas = [] + + for strip in strips: + data = [] + first = strip['first'] + + def emitData(): + stripData = { + 'first': strip['first'], + 'data': bytes(buffer) + } + + for i in range(strip['count']): + color = shader(position) + data += [color] + + emitData() + + stripDatas += [stripData] + return stripData + + +def sendSequence(sock, start, data, d = 1): + + # https://kno.wled.ge/interfaces/udp-realtime/ + #FIXME: Somehow 489 does not work + maxChunk = 489-10 + while len(data) > 0: + + buf = bytearray() + + buf += bytes([4, d]) # DNRGB, Number of seconds before normal mode + + buf += bytes([(start >> 8) & 0xFF]) + buf += bytes([(start >> 0) & 0xFF]) + + buf += data[0:maxChunk*3] + data = data[maxChunk*3:] + start += maxChunk + + sock.send(buf) + +def fade(a, b, count): + buf = [] + for i in range(count): + p = i / (count - 1) + buf += lerp(a, b, p) + return buf + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7528dd --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## LeitLightLightLeitLeidLite + +LeitLightLightLeitLeidLite macht dein Leid mit den Leitstellen Lights Lite'r indem es Light Leitet. diff --git a/test.py b/test.py new file mode 100755 index 0000000..3aeae77 --- /dev/null +++ b/test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import LeitLightLightLeitLeidLite as l6 + +rows = l6.rows + +# Export 3D model of physical arrangement +sourceStrips = l6.getSourceStrips(rows) +print(sourceStrips) +strips = l6.bakeStrips(sourceStrips) +obj = l6.generateObj(strips) +open("/tmp/strips.obj", "w").write(obj) +obj = l6.generateObj(strips, True) +open("/tmp/strips-bounds.obj", "w").write(obj) + +# From behind of plane to above plane +print(l6.debugStrip(l6.clipStripAgainstPlane(l6.testStrip((-10, 0, 0), (10, 0, 0), 21), (0, 0, 0), (1, 0, 0)))) + +# From above of plane to behind plane +print(l6.debugStrip(l6.clipStripAgainstPlane(l6.testStrip((10, 0, 0), (-10, 0, 0), 21), (0, 0, 0), (1, 0, 0)))) + +# Parallel above of plane +print(l6.debugStrip(l6.clipStripAgainstPlane(l6.testStrip((10, -10, 0), (10, 10, 0), 21), (0, 0, 0), (1, 0, 0)))) + +# Parallel behind of plane +print(l6.debugStrip(l6.clipStripAgainstPlane(l6.testStrip((-10, -10, 0), (-10, 10, 0), 21), (0, 0, 0), (1, 0, 0)))) + + + +stripsXp = l6.clipStripsAgainstPlane(strips, (100, 0, 0), (1, 0, 0)) +obj = l6.generateObj(stripsXp) +open("/tmp/strips-xp.obj", "w").write(obj) + +stripsYp = l6.clipStripsAgainstPlane(strips, (0, 50, 0), (0, 1, 0)) +obj = l6.generateObj(stripsYp) +open("/tmp/strips-yp.obj", "w").write(obj) + +stripsAabb = l6.clipStripsAgainstAabb(strips, (80, 60/2, -100), (120, 140/2, +100)) +obj = l6.generateObj(stripsAabb) +open("/tmp/strips-aabb.obj", "w").write(obj) +