903 lines
39 KiB
Python
903 lines
39 KiB
Python
#!/usr/bin/env python
|
|
# pylddlib version 0.4.9.7
|
|
# based on pyldd2obj version 0.4.8 - Copyright (c) 2019 by jonnysp
|
|
#
|
|
# Updates:
|
|
# 0.4.9.8 Make work with LEGO Universe brickdb
|
|
# 0.4.9.7 corrected bug of incorrectly parsing the primitive xml file, specifically with comments. Add support LDDLIFTREE envirnment variable to set location of db.lif.
|
|
# 0.4.9.6 preliminary Linux support
|
|
# 0.4.9.5 corrected bug of incorrectly Bounding / GeometryBounding parsing the primitive xml file.
|
|
# 0.4.9.4 improved lif.db checking for crucial files (because of the infamous botched 4.3.12 LDD Windows update).
|
|
# 0.4.9.3 improved Windows and Python 3 compatibility
|
|
# 0.4.9.2 changed handling of material = 0 for a part. Now a 0 will choose the 1st material (the base material of a part) and not the previous material of the subpart before. This will fix "Chicken Helmet Part 11262". It may break other parts and this change needs further regression.
|
|
# 0.4.9.1 improved custom2DField handling, fixed decorations bug, improved material assignments handling
|
|
# 0.4.9 updates to support reading extracted db.lif from db folder
|
|
#
|
|
# License: MIT License
|
|
#
|
|
|
|
import os
|
|
import platform
|
|
import sys
|
|
import math
|
|
import struct
|
|
import zipfile
|
|
from xml.dom import minidom
|
|
import time
|
|
|
|
if sys.version_info < (3, 0):
|
|
reload(sys)
|
|
sys.setdefaultencoding('utf-8')
|
|
|
|
PRIMITIVEPATH = '/Primitives/'
|
|
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
|
|
DECORATIONPATH = '/Decorations/'
|
|
MATERIALNAMESPATH = '/MaterialNames/'
|
|
|
|
LOGOONSTUDSCONNTYPE = {"0:4", "0:4:1", "0:4:2", "0:4:33", "2:4:1", "2:4:34"}
|
|
|
|
class Matrix3D:
|
|
def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1):
|
|
self.n11 = n11
|
|
self.n12 = n12
|
|
self.n13 = n13
|
|
self.n14 = n14
|
|
self.n21 = n21
|
|
self.n22 = n22
|
|
self.n23 = n23
|
|
self.n24 = n24
|
|
self.n31 = n31
|
|
self.n32 = n32
|
|
self.n33 = n33
|
|
self.n34 = n34
|
|
self.n41 = n41
|
|
self.n42 = n42
|
|
self.n43 = n43
|
|
self.n44 = n44
|
|
|
|
def __str__(self):
|
|
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(self.n11, self.n12, self.n13,self.n14,self.n21, self.n22, self.n23,self.n24,self.n31, self.n32, self.n33,self.n34,self.n41, self.n42, self.n43,self.n44)
|
|
|
|
def rotate(self,angle=0,axis=0):
|
|
c = math.cos(angle)
|
|
s = math.sin(angle)
|
|
t = 1 - c
|
|
|
|
tx = t * axis.x
|
|
ty = t * axis.y
|
|
tz = t * axis.z
|
|
|
|
sx = s * axis.x
|
|
sy = s * axis.y
|
|
sz = s * axis.z
|
|
|
|
self.n11 = c + axis.x * tx
|
|
self.n12 = axis.y * tx + sz
|
|
self.n13 = axis.z * tx - sy
|
|
self.n14 = 0
|
|
|
|
self.n21 = axis.x * ty - sz
|
|
self.n22 = c + axis.y * ty
|
|
self.n23 = axis.z * ty + sx
|
|
self.n24 = 0
|
|
|
|
self.n31 = axis.x * tz + sy
|
|
self.n32 = axis.y * tz - sx
|
|
self.n33 = c + axis.z * tz
|
|
self.n34 = 0
|
|
|
|
self.n41 = 0
|
|
self.n42 = 0
|
|
self.n43 = 0
|
|
self.n44 = 1
|
|
|
|
def __mul__(self, other):
|
|
return Matrix3D(
|
|
self.n11 * other.n11 + self.n21 * other.n12 + self.n31 * other.n13 + self.n41 * other.n14,
|
|
self.n12 * other.n11 + self.n22 * other.n12 + self.n32 * other.n13 + self.n42 * other.n14,
|
|
self.n13 * other.n11 + self.n23 * other.n12 + self.n33 * other.n13 + self.n43 * other.n14,
|
|
self.n14 * other.n11 + self.n24 * other.n12 + self.n34 * other.n13 + self.n44 * other.n14,
|
|
self.n11 * other.n21 + self.n21 * other.n22 + self.n31 * other.n23 + self.n41 * other.n24,
|
|
self.n12 * other.n21 + self.n22 * other.n22 + self.n32 * other.n23 + self.n42 * other.n24,
|
|
self.n13 * other.n21 + self.n23 * other.n22 + self.n33 * other.n23 + self.n43 * other.n24,
|
|
self.n14 * other.n21 + self.n24 * other.n22 + self.n34 * other.n23 + self.n44 * other.n24,
|
|
self.n11 * other.n31 + self.n21 * other.n32 + self.n31 * other.n33 + self.n41 * other.n34,
|
|
self.n12 * other.n31 + self.n22 * other.n32 + self.n32 * other.n33 + self.n42 * other.n34,
|
|
self.n13 * other.n31 + self.n23 * other.n32 + self.n33 * other.n33 + self.n43 * other.n34,
|
|
self.n14 * other.n31 + self.n24 * other.n32 + self.n34 * other.n33 + self.n44 * other.n34,
|
|
self.n11 * other.n41 + self.n21 * other.n42 + self.n31 * other.n43 + self.n41 * other.n44,
|
|
self.n12 * other.n41 + self.n22 * other.n42 + self.n32 * other.n43 + self.n42 * other.n44,
|
|
self.n13 * other.n41 + self.n23 * other.n42 + self.n33 * other.n43 + self.n43 * other.n44,
|
|
self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44
|
|
)
|
|
|
|
class Point3D:
|
|
def __init__(self, x=0,y=0,z=0):
|
|
self.x = x
|
|
self.y = y
|
|
self.z = z
|
|
|
|
def __str__(self):
|
|
return '[{0},{1},{2}]'.format(self.x, self.y,self.z)
|
|
|
|
def string(self,prefix = "v"):
|
|
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z)
|
|
|
|
def transformW(self,matrix):
|
|
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z
|
|
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z
|
|
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z
|
|
self.x = x
|
|
self.y = y
|
|
self.z = z
|
|
|
|
def transform(self,matrix):
|
|
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41
|
|
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42
|
|
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43
|
|
self.x = x
|
|
self.y = y
|
|
self.z = z
|
|
|
|
def copy(self):
|
|
return Point3D(x=self.x,y=self.y,z=self.z)
|
|
|
|
class Point2D:
|
|
def __init__(self, x=0,y=0):
|
|
self.x = x
|
|
self.y = y
|
|
def __str__(self):
|
|
return '[{0},{1}]'.format(self.x, self.y * -1)
|
|
def string(self,prefix="t"):
|
|
return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 )
|
|
def copy(self):
|
|
return Point2D(x=self.x,y=self.y)
|
|
|
|
class Face:
|
|
def __init__(self,a=0,b=0,c=0):
|
|
self.a = a
|
|
self.b = b
|
|
self.c = c
|
|
def string(self,prefix="f", indexOffset=0 ,textureoffset=0):
|
|
if textureoffset == 0:
|
|
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset)
|
|
else:
|
|
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset)
|
|
def __str__(self):
|
|
return '[{0},{1},{2}]'.format(self.a, self.b, self.c)
|
|
|
|
class Group:
|
|
def __init__(self, node):
|
|
self.partRefs = node.getAttribute('partRefs').split(',')
|
|
|
|
class Bone:
|
|
def __init__(self, node):
|
|
self.refID = node.getAttribute('refID')
|
|
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
|
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
|
|
|
class Part:
|
|
def __init__(self, node):
|
|
self.isGrouped = False
|
|
self.GroupIDX = 0
|
|
self.Bones = []
|
|
self.refID = node.getAttribute('refID')
|
|
self.designID = node.getAttribute('designID')
|
|
self.materials = list(map(str, node.getAttribute('materials').split(',')))
|
|
|
|
lastm = '0'
|
|
for i, m in enumerate(self.materials):
|
|
if (m == '0'):
|
|
# self.materials[i] = lastm
|
|
self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material
|
|
else:
|
|
lastm = m
|
|
if node.hasAttribute('decoration'):
|
|
self.decoration = list(map(str,node.getAttribute('decoration').split(',')))
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Bone':
|
|
self.Bones.append(Bone(node=childnode))
|
|
|
|
class Brick:
|
|
def __init__(self, node):
|
|
self.refID = node.getAttribute('refID')
|
|
self.designID = node.getAttribute('designID')
|
|
self.Parts = []
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Part':
|
|
self.Parts.append(Part(node=childnode))
|
|
|
|
class SceneCamera:
|
|
def __init__(self, node):
|
|
self.refID = node.getAttribute('refID')
|
|
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
|
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
|
self.fieldOfView = float(node.getAttribute('fieldOfView'))
|
|
self.distance = float(node.getAttribute('distance'))
|
|
|
|
class Scene:
|
|
def __init__(self, file):
|
|
self.Bricks = []
|
|
self.Scenecamera = []
|
|
self.Groups = []
|
|
|
|
if file.endswith('.lxfml'):
|
|
with open(file, "rb") as file:
|
|
data = file.read()
|
|
elif file.endswith('.lxf'):
|
|
zf = zipfile.ZipFile(file, 'r')
|
|
data = zf.read('IMAGE100.LXFML')
|
|
else:
|
|
return
|
|
|
|
xml = minidom.parseString(data)
|
|
self.Name = xml.firstChild.getAttribute('name')
|
|
|
|
for node in xml.firstChild.childNodes:
|
|
if node.nodeName == 'Meta':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'BrickSet':
|
|
self.Version = str(childnode.getAttribute('version'))
|
|
elif node.nodeName == 'Cameras':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Camera':
|
|
self.Scenecamera.append(SceneCamera(node=childnode))
|
|
elif node.nodeName == 'Bricks':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Brick':
|
|
self.Bricks.append(Brick(node=childnode))
|
|
elif node.nodeName == 'GroupSystems':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'GroupSystem':
|
|
for childnode in childnode.childNodes:
|
|
if childnode.nodeName == 'Group':
|
|
self.Groups.append(Group(node=childnode))
|
|
|
|
for i in range(len(self.Groups)):
|
|
for brick in self.Bricks:
|
|
for part in brick.Parts:
|
|
if part.refID in self.Groups[i].partRefs:
|
|
part.isGrouped = True
|
|
part.GroupIDX = i
|
|
|
|
# print('Scene "'+ self.Name + '" Brickversion: ' + str(self.Version))
|
|
|
|
class GeometryReader:
|
|
def __init__(self, data):
|
|
self.offset = 0
|
|
self.data = data
|
|
self.positions = []
|
|
self.normals = []
|
|
self.textures = []
|
|
self.faces = []
|
|
self.bonemap = {}
|
|
self.texCount = 0
|
|
self.outpositions = []
|
|
self.outnormals = []
|
|
|
|
if self.readInt() == 1111961649:
|
|
self.valueCount = self.readInt()
|
|
self.indexCount = self.readInt()
|
|
self.faceCount = int(self.indexCount / 3)
|
|
options = self.readInt()
|
|
|
|
for i in range(0, self.valueCount):
|
|
self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
|
|
|
for i in range(0, self.valueCount):
|
|
self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
|
|
|
if (options & 3) == 3:
|
|
self.texCount = self.valueCount
|
|
for i in range(0, self.valueCount):
|
|
self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat()))
|
|
|
|
for i in range(0, self.faceCount):
|
|
self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt()))
|
|
|
|
if (options & 48) == 48:
|
|
num = self.readInt()
|
|
self.offset += (num * 4) + (self.indexCount * 4)
|
|
num = self.readInt()
|
|
self.offset += (3 * num * 4) + (self.indexCount * 4)
|
|
|
|
bonelength = self.readInt()
|
|
self.bonemap = [0] * self.valueCount
|
|
|
|
if (bonelength > self.valueCount) or (bonelength > self.faceCount):
|
|
datastart = self.offset
|
|
self.offset += bonelength
|
|
for i in range(0, self.valueCount):
|
|
boneoffset = self.readInt() + 4
|
|
self.bonemap[i] = self.read_Int(datastart + boneoffset)
|
|
|
|
def read_Int(self,_offset):
|
|
if sys.version_info < (3, 0):
|
|
return int(struct.unpack_from('i', self.data, _offset)[0])
|
|
else:
|
|
return int.from_bytes(self.data[_offset:_offset + 4], byteorder='little')
|
|
|
|
def readInt(self):
|
|
if sys.version_info < (3, 0):
|
|
ret = int(struct.unpack_from('i', self.data, self.offset)[0])
|
|
else:
|
|
ret = int.from_bytes(self.data[self.offset:self.offset + 4], byteorder='little')
|
|
self.offset += 4
|
|
return ret
|
|
|
|
def readFloat(self):
|
|
ret = float(struct.unpack_from('f', self.data, self.offset)[0])
|
|
self.offset += 4
|
|
return ret
|
|
|
|
class Geometry:
|
|
def __init__(self, designID, database):
|
|
self.designID = designID
|
|
self.Parts = {}
|
|
self.maxGeoBounding = -1
|
|
self.studsFields2D = []
|
|
|
|
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID,'.g')
|
|
GeometryCount = 0
|
|
while str(GeometryLocation) in database.filelist:
|
|
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
|
|
GeometryCount += 1
|
|
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID,'.g',GeometryCount)
|
|
|
|
primitive = Primitive(data = database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
|
|
self.Partname = primitive.Designname
|
|
self.studsFields2D = primitive.Fields2D
|
|
try:
|
|
geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))]
|
|
geoBoundingList.sort()
|
|
self.maxGeoBounding = geoBoundingList[-1]
|
|
except KeyError as e:
|
|
# print('\nBounding errror in part {0}: {1}\n'.format(designID, e))
|
|
pass
|
|
|
|
# preflex
|
|
for part in self.Parts:
|
|
# transform
|
|
for i, b in enumerate(primitive.Bones):
|
|
# positions
|
|
for j, p in enumerate(self.Parts[part].positions):
|
|
if (self.Parts[part].bonemap[j] == i):
|
|
self.Parts[part].positions[j].transform(b.matrix)
|
|
# normals
|
|
for k, n in enumerate(self.Parts[part].normals):
|
|
if (self.Parts[part].bonemap[k] == i):
|
|
self.Parts[part].normals[k].transformW(b.matrix)
|
|
|
|
def valuecount(self):
|
|
count = 0
|
|
for part in self.Parts:
|
|
count += self.Parts[part].valueCount
|
|
return count
|
|
|
|
def facecount(self):
|
|
count = 0
|
|
for part in self.Parts:
|
|
count += self.Parts[part].faceCount
|
|
return count
|
|
|
|
def texcount(self):
|
|
count = 0
|
|
for part in self.Parts:
|
|
count += self.Parts[part].texCount
|
|
return count
|
|
|
|
class Bone2:
|
|
def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
|
self.boneId = boneId
|
|
rotationMatrix = Matrix3D()
|
|
rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az))
|
|
p = Point3D(x=tx,y=ty,z=tz)
|
|
p.transformW(rotationMatrix)
|
|
rotationMatrix.n41 -= p.x
|
|
rotationMatrix.n42 -= p.y
|
|
rotationMatrix.n43 -= p.z
|
|
self.matrix = rotationMatrix
|
|
|
|
class Field2D:
|
|
def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'):
|
|
self.type = type
|
|
self.field2DRawData = field2DRawData
|
|
rotationMatrix = Matrix3D()
|
|
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
|
p = Point3D(x=tx,y=ty,z=tz)
|
|
p.transformW(rotationMatrix)
|
|
rotationMatrix.n41 -= p.x
|
|
rotationMatrix.n42 -= p.y
|
|
rotationMatrix.n43 -= p.z
|
|
|
|
self.matrix = rotationMatrix
|
|
self.custom2DField = []
|
|
|
|
#The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
|
|
rows_count = height + 1
|
|
cols_count = width + 1
|
|
# creation looks reverse
|
|
# create an array of "cols_count" cols, for each of the "rows_count" rows
|
|
# all elements are initialized to 0
|
|
self.custom2DField = [[0 for j in range(cols_count)] for i in range(rows_count)]
|
|
custom2DFieldString = field2DRawData.replace('\r', '').replace('\n', '').replace(' ', '')
|
|
custom2DFieldArr = custom2DFieldString.strip().split(',')
|
|
|
|
k = 0
|
|
for i in range(rows_count):
|
|
for j in range(cols_count):
|
|
self.custom2DField[i][j] = custom2DFieldArr[k]
|
|
k += 1
|
|
|
|
def __str__(self):
|
|
return '[type="{0}" transform="{1}" custom2DField="{2}"]'.format(self.type, self.matrix, self.custom2DField)
|
|
|
|
class CollisionBox:
|
|
def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
|
rotationMatrix = Matrix3D()
|
|
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
|
p = Point3D(x=tx,y=ty,z=tz)
|
|
p.transformW(rotationMatrix)
|
|
rotationMatrix.n41 -= p.x
|
|
rotationMatrix.n42 -= p.y
|
|
rotationMatrix.n43 -= p.z
|
|
|
|
self.matrix = rotationMatrix
|
|
self.corner = Point3D(x=sX,y=sY,z=sZ)
|
|
self.positions = []
|
|
|
|
self.positions.append(Point3D(x=0, y=0, z=0))
|
|
self.positions.append(Point3D(x=sX, y=0, z=0))
|
|
self.positions.append(Point3D(x=0, y=sY, z=0))
|
|
self.positions.append(Point3D(x=sX, y=sY, z=0))
|
|
self.positions.append(Point3D(x=0, y=0, z=sZ))
|
|
self.positions.append(Point3D(x=0, y=sY, z=sZ))
|
|
self.positions.append(Point3D(x=sX ,y=0, z=sZ))
|
|
self.positions.append(Point3D(x=sX ,y=sY, z=sZ))
|
|
|
|
def __str__(self):
|
|
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(self.corner.x, self.corner.y, self.corner.z)
|
|
|
|
class Primitive:
|
|
def __init__(self, data):
|
|
self.Designname = ''
|
|
self.Bones = []
|
|
self.Fields2D = []
|
|
self.CollisionBoxes = []
|
|
self.PhysicsAttributes = {}
|
|
self.Bounding = {}
|
|
self.GeometryBounding = {}
|
|
xml = minidom.parseString(data)
|
|
root = xml.documentElement
|
|
for node in root.childNodes:
|
|
if node.__class__.__name__.lower() == 'comment':
|
|
self.comment = node[0].nodeValue
|
|
if node.nodeName == 'Flex':
|
|
for node in node.childNodes:
|
|
if node.nodeName == 'Bone':
|
|
self.Bones.append(Bone2(boneId=int(node.getAttribute('boneId')), angle=float(node.getAttribute('angle')), ax=float(node.getAttribute('ax')), ay=float(node.getAttribute('ay')), az=float(node.getAttribute('az')), tx=float(node.getAttribute('tx')), ty=float(node.getAttribute('ty')), tz=float(node.getAttribute('tz'))))
|
|
elif node.nodeName == 'Annotations':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Annotation' and childnode.hasAttribute('designname'):
|
|
self.Designname = childnode.getAttribute('designname')
|
|
elif node.nodeName == 'Collision':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Box':
|
|
self.CollisionBoxes.append(CollisionBox(sX=float(childnode.getAttribute('sX')), sY=float(childnode.getAttribute('sY')), sZ=float(childnode.getAttribute('sZ')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz'))))
|
|
elif node.nodeName == 'PhysicsAttributes':
|
|
self.PhysicsAttributes = {"inertiaTensor": node.getAttribute('inertiaTensor'),"centerOfMass": node.getAttribute('centerOfMass'),"mass": node.getAttribute('mass'),"frictionType": node.getAttribute('frictionType')}
|
|
elif node.nodeName == 'Bounding':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'AABB':
|
|
self.Bounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
|
elif node.nodeName == 'GeometryBounding':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'AABB':
|
|
self.GeometryBounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
|
elif node.nodeName == 'Connectivity':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Custom2DField':
|
|
self.Fields2D.append(Field2D(type=int(childnode.getAttribute('type')), width=int(childnode.getAttribute('width')), height=int(childnode.getAttribute('height')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')), field2DRawData=str(childnode.firstChild.data)))
|
|
elif node.nodeName == 'Decoration':
|
|
self.Decoration = {"faces": node.getAttribute('faces'), "subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')}
|
|
|
|
class Materials:
|
|
def __init__(self, data):
|
|
self.Materials = {}
|
|
xml = minidom.parseString(data)
|
|
for node in xml.firstChild.childNodes:
|
|
if node.nodeName == 'Material':
|
|
self.Materials[node.getAttribute('MatID')] = Material(
|
|
node.getAttribute('MatID'),
|
|
r=int(node.getAttribute('Red')),
|
|
g=int(node.getAttribute('Green')),
|
|
b=int(node.getAttribute('Blue')),
|
|
a=int(node.getAttribute('Alpha')),
|
|
mtype=str(node.getAttribute('MaterialType'))
|
|
)
|
|
|
|
def getMaterialbyId(self, mid):
|
|
return self.Materials[mid]
|
|
|
|
class Material:
|
|
def __init__(self,id, r, g, b, a, mtype):
|
|
self.id = id
|
|
self.name = id
|
|
self.mattype = mtype
|
|
self.r = float(r)
|
|
self.g = float(g)
|
|
self.b = float(b)
|
|
self.a = float(a)
|
|
def string(self):
|
|
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format( self.r / 255, self.g / 255,self.b / 255)
|
|
if self.a < 255:
|
|
out += 'Ni 1.575\n' + 'd {0}'.format(0.05) + '\n' + 'Tr {0}\n'.format(0.05)
|
|
return out
|
|
|
|
class DBinfo:
|
|
def __init__(self, data):
|
|
xml = minidom.parseString(data)
|
|
self.Version = xml.getElementsByTagName('Bricks')[0].attributes['version'].value
|
|
# print('DB Version: ' + str(self.Version))
|
|
|
|
class DBFolderFile:
|
|
def __init__(self, name, handle):
|
|
self.handle = handle
|
|
self.name = name
|
|
|
|
def read(self):
|
|
reader = open(self.handle, "rb")
|
|
try:
|
|
filecontent = reader.read()
|
|
reader.close()
|
|
return filecontent
|
|
finally:
|
|
reader.close()
|
|
|
|
class LIFFile:
|
|
def __init__(self, name, offset, size, handle):
|
|
self.handle = handle
|
|
self.name = name
|
|
self.offset = offset
|
|
self.size = size
|
|
|
|
def read(self):
|
|
self.handle.seek(self.offset, 0)
|
|
return self.handle.read(self.size)
|
|
|
|
class DBFolderReader:
|
|
def __init__(self, folder):
|
|
self.filelist = {}
|
|
self.initok = False
|
|
self.location = folder
|
|
self.dbinfo = None
|
|
|
|
try:
|
|
os.path.isdir(self.location)
|
|
except Exception as e:
|
|
self.initok = False
|
|
# print("db folder read FAIL")
|
|
return
|
|
else:
|
|
self.parse()
|
|
if self.fileexist(os.path.join(self.location,'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
|
|
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location,'info.xml')].read())
|
|
# print("DB folder OK.")
|
|
self.initok = True
|
|
else:
|
|
# print("DB folder ERROR")
|
|
# print(os.path.join(self.location,'Materials.xml'))
|
|
# print(self.fileexist(os.path.join(self.location,'Materials.xml')))
|
|
# print(os.path.join(self.location,'info.xml'))
|
|
# print(self.fileexist(os.path.join(self.location, 'info.xml')))
|
|
# print(MATERIALNAMESPATH)
|
|
pass
|
|
|
|
|
|
def fileexist(self, filename):
|
|
return filename in self.filelist
|
|
|
|
def parse(self):
|
|
for path, subdirs, files in os.walk(self.location):
|
|
for name in files:
|
|
entryName = os.path.join(path, name)
|
|
self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName)
|
|
|
|
class LIFReader:
|
|
def __init__(self, file):
|
|
self.packedFilesOffset = 84
|
|
self.filelist = {}
|
|
self.initok = False
|
|
self.location = file
|
|
self.dbinfo = None
|
|
|
|
try:
|
|
self.filehandle = open(self.location, "rb")
|
|
self.filehandle.seek(0, 0)
|
|
except Exception as e:
|
|
self.initok = False
|
|
# print("Database FAIL")
|
|
return
|
|
else:
|
|
if self.filehandle.read(4).decode() == "LIFF":
|
|
self.parse(prefix='', offset=self.readInt(offset=72) + 64)
|
|
if self.fileexist('/Materials.xml') and self.fileexist('/info.xml'):
|
|
self.dbinfo = DBinfo(data=self.filelist['/info.xml'].read())
|
|
# print("Database OK.")
|
|
self.initok = True
|
|
else:
|
|
# print("Database ERROR")
|
|
pass
|
|
else:
|
|
# print("Database FAIL")
|
|
self.initok = False
|
|
|
|
def fileexist(self,filename):
|
|
return filename in self.filelist
|
|
|
|
def parse(self, prefix='', offset=0):
|
|
if prefix == '':
|
|
offset += 36
|
|
else:
|
|
offset += 4
|
|
|
|
count = self.readInt(offset=offset)
|
|
|
|
for i in range(0, count):
|
|
offset += 4
|
|
entryType = self.readShort(offset=offset)
|
|
offset += 6
|
|
|
|
entryName = '{0}{1}'.format(prefix,'/');
|
|
self.filehandle.seek(offset + 1, 0)
|
|
if sys.version_info < (3, 0):
|
|
t = ord(self.filehandle.read(1))
|
|
else:
|
|
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
|
|
|
|
while not t == 0:
|
|
entryName ='{0}{1}'.format(entryName,chr(t))
|
|
self.filehandle.seek(1, 1)
|
|
if sys.version_info < (3, 0):
|
|
t = ord(self.filehandle.read(1))
|
|
else:
|
|
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
|
|
|
|
offset += 2
|
|
|
|
offset += 6
|
|
self.packedFilesOffset += 20
|
|
|
|
if entryType == 1:
|
|
offset = self.parse(prefix=entryName, offset=offset)
|
|
elif entryType == 2:
|
|
fileSize = self.readInt(offset=offset) - 20
|
|
self.filelist[entryName] = LIFFile(name=entryName, offset=self.packedFilesOffset, size=fileSize, handle=self.filehandle)
|
|
offset += 24
|
|
self.packedFilesOffset += fileSize
|
|
|
|
return offset
|
|
|
|
def readInt(self, offset=0):
|
|
self.filehandle.seek(offset, 0)
|
|
if sys.version_info < (3, 0):
|
|
return int(struct.unpack('>i', self.filehandle.read(4))[0])
|
|
else:
|
|
return int.from_bytes(self.filehandle.read(4), byteorder='big')
|
|
|
|
def readShort(self, offset=0):
|
|
self.filehandle.seek(offset, 0)
|
|
if sys.version_info < (3, 0):
|
|
return int(struct.unpack('>h', self.filehandle.read(2))[0])
|
|
else:
|
|
return int.from_bytes(self.filehandle.read(2), byteorder='big')
|
|
|
|
class Converter:
|
|
def LoadDBFolder(self, dbfolderlocation):
|
|
self.database = DBFolderReader(folder=dbfolderlocation)
|
|
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation,'Materials.xml')):
|
|
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation,'Materials.xml')].read());
|
|
|
|
def LoadDatabase(self,databaselocation):
|
|
self.database = LIFReader(file=databaselocation)
|
|
|
|
if self.database.initok and self.database.fileexist('/Materials.xml'):
|
|
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read());
|
|
|
|
def LoadScene(self,filename):
|
|
if self.database.initok:
|
|
self.scene = Scene(file=filename)
|
|
|
|
def Export(self,filename):
|
|
invert = Matrix3D()
|
|
#invert.n33 = -1 #uncomment to invert the Z-Axis
|
|
|
|
indexOffset = 1
|
|
textOffset = 1
|
|
usedmaterials = []
|
|
geometriecache = {}
|
|
|
|
start_time = time.time()
|
|
|
|
out = open(filename + ".obj.tmp", "w+")
|
|
out.write("mtllib " + filename + ".mtl" + '\n\n')
|
|
outtext = open(filename + ".mtl.tmp", "w+")
|
|
|
|
total = len(self.scene.Bricks)
|
|
current = 0
|
|
|
|
for bri in self.scene.Bricks:
|
|
current += 1
|
|
|
|
for pa in bri.Parts:
|
|
|
|
if pa.designID not in geometriecache:
|
|
geo = Geometry(designID=pa.designID, database=self.database)
|
|
progress(current ,total , "(" + geo.designID + ") " + geo.Partname, ' ')
|
|
geometriecache[pa.designID] = geo
|
|
else:
|
|
geo = geometriecache[pa.designID]
|
|
|
|
progress(current ,total , "(" + geo.designID + ") " + geo.Partname ,'-')
|
|
|
|
out.write("o\n")
|
|
|
|
for part in geo.Parts:
|
|
geo.Parts[part].outpositions = [elem.copy() for elem in geo.Parts[part].positions]
|
|
geo.Parts[part].outnormals = [elem.copy() for elem in geo.Parts[part].normals]
|
|
|
|
for i, b in enumerate(pa.Bones):
|
|
# positions
|
|
for j, p in enumerate(geo.Parts[part].outpositions):
|
|
if (geo.Parts[part].bonemap[j] == i):
|
|
p.transform( invert * b.matrix)
|
|
# normals
|
|
for k, n in enumerate(geo.Parts[part].outnormals):
|
|
if (geo.Parts[part].bonemap[k] == i):
|
|
n.transformW( invert * b.matrix)
|
|
|
|
for point in geo.Parts[part].outpositions:
|
|
out.write(point.string("v"))
|
|
|
|
for normal in geo.Parts[part].outnormals:
|
|
out.write(normal.string("vn"))
|
|
|
|
for text in geo.Parts[part].textures:
|
|
out.write(text.string("vt"))
|
|
|
|
decoCount = 0
|
|
out.write("g " + "(" + geo.designID + ") " + geo.Partname + '\n')
|
|
last_color = 0
|
|
for part in geo.Parts:
|
|
|
|
|
|
#try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
|
|
try:
|
|
materialCurrentPart = pa.materials[part]
|
|
last_color = pa.materials[part]
|
|
except IndexError:
|
|
# print('WARNING: {0}.g{1} has NO material assignment in lxf. Replaced with color {2}. Fix {0}.xml faces values.'.format(pa.designID, part, last_color))
|
|
materialCurrentPart = last_color
|
|
|
|
lddmat = self.allMaterials.getMaterialbyId(materialCurrentPart)
|
|
matname = lddmat.name
|
|
|
|
deco = '0'
|
|
if hasattr(pa, 'decoration') and len(geo.Parts[part].textures) > 0:
|
|
#if decoCount <= len(pa.decoration):
|
|
if decoCount < len(pa.decoration):
|
|
deco = pa.decoration[decoCount]
|
|
decoCount += 1
|
|
|
|
extfile = ''
|
|
if not deco == '0':
|
|
extfile = deco + '.png'
|
|
matname += "_" + deco
|
|
decofilename = DECORATIONPATH + deco + '.png'
|
|
if not os.path.isfile(extfile) and self.database.fileexist(decofilename):
|
|
with open(extfile, "wb") as f:
|
|
f.write(self.database.filelist[decofilename].read())
|
|
f.close()
|
|
|
|
if not matname in usedmaterials:
|
|
usedmaterials.append(matname)
|
|
outtext.write("newmtl " + matname + '\n')
|
|
outtext.write(lddmat.string())
|
|
if not deco == '0':
|
|
outtext.write("map_Kd " + deco + ".png" + '\n')
|
|
|
|
out.write("usemtl " + matname + '\n')
|
|
for face in geo.Parts[part].faces:
|
|
if len(geo.Parts[part].textures) > 0:
|
|
out.write(face.string("f",indexOffset,textOffset))
|
|
else:
|
|
out.write(face.string("f",indexOffset))
|
|
|
|
indexOffset += len(geo.Parts[part].outpositions)
|
|
textOffset += len(geo.Parts[part].textures)
|
|
# -----------------------------------------------------------------
|
|
out.write('\n')
|
|
os.rename(filename + ".obj.tmp", filename + ".obj")
|
|
os.rename(filename + ".mtl.tmp", filename + ".mtl")
|
|
|
|
sys.stdout.write('%s\r' % (' '))
|
|
# print("--- %s seconds ---" % (time.time() - start_time))
|
|
|
|
def setDBFolderVars(dbfolderlocation):
|
|
global PRIMITIVEPATH
|
|
global GEOMETRIEPATH
|
|
global DECORATIONPATH
|
|
global MATERIALNAMESPATH
|
|
PRIMITIVEPATH = os.path.join(dbfolderlocation, 'Primitives', '')
|
|
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', 'lod0', '')
|
|
DECORATIONPATH = os.path.join(dbfolderlocation, 'Decorations', '')
|
|
MATERIALNAMESPATH = os.path.join(dbfolderlocation, 'MaterialNames', '')
|
|
# print(MATERIALNAMESPATH)
|
|
|
|
def FindDatabase():
|
|
lddliftree = os.getenv('LDDLIFTREE')
|
|
if lddliftree is not None:
|
|
if os.path.isdir(str(lddliftree)): #LDDLIFTREE points to folder
|
|
return str(lddliftree)
|
|
elif os.path.isfile(str(lddliftree)): #LDDLIFTREE points to file (should be db.lif)
|
|
return str(lddliftree)
|
|
|
|
else: #Env variable LDDLIFTREE not set. Check for default locations per different platform.
|
|
if platform.system() == 'Darwin':
|
|
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))
|
|
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))
|
|
else:
|
|
# print("no LDD database found please install LEGO-Digital-Designer")
|
|
os._exit()
|
|
elif platform.system() == 'Windows':
|
|
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))
|
|
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))
|
|
else:
|
|
# print("no LDD database found please install LEGO-Digital-Designer")
|
|
os._exit()
|
|
elif platform.system() == 'Linux':
|
|
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))
|
|
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))):
|
|
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))
|
|
else:
|
|
# print("no LDD database found please install LEGO-Digital-Designer")
|
|
os._exit()
|
|
else:
|
|
# print('Your OS {0} is not supported yet.'.format(platform.system()))
|
|
os._exit()
|
|
|
|
def progress(count, total, status='', suffix = ''):
|
|
bar_len = 40
|
|
filled_len = int(round(bar_len * count / float(total)))
|
|
percents = round(100.0 * count / float(total), 1)
|
|
bar = '#' * filled_len + '-' * (bar_len - filled_len)
|
|
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, ' '))
|
|
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, status))
|
|
sys.stdout.flush()
|
|
|
|
def main(lxf_filename, obj_filename):
|
|
# print("- - - pylddlib - - -")
|
|
# print(" _ ")
|
|
# print(" [_]")
|
|
# print(" /| |\\")
|
|
# print(" ()'---' C")
|
|
# print(" | | |")
|
|
# print(" [=|=]")
|
|
# print("")
|
|
# print("- - - - - - - - - - - -")
|
|
|
|
converter = Converter()
|
|
# print("Found DB folder. Will use this instead of db.lif!")
|
|
setDBFolderVars(dbfolderlocation = "app/luclient/res/")
|
|
converter.LoadDBFolder(dbfolderlocation = "app/luclient/res/")
|
|
converter.LoadScene(filename=lxf_filename)
|
|
converter.Export(filename=obj_filename)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|