Initial snapshot

This commit is contained in:
vogelj 2025-02-11 21:48:13 +01:00
commit 659d9dccbe
3 changed files with 370 additions and 0 deletions

View file

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

3
README.md Normal file
View file

@ -0,0 +1,3 @@
## LeitLightLightLeitLeidLite
LeitLightLightLeitLeidLite macht dein Leid mit den Leitstellen Lights Lite'r indem es Light Leitet.

41
test.py Executable file
View file

@ -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)