mirror of
https://github.com/Squareville/lu-toolbox.git
synced 2026-01-01 16:49:53 -06:00
1183 lines
43 KiB
Python
1183 lines
43 KiB
Python
# based on pyldd2obj by jonnysp and lxfml import plugin by sttng
|
|
# modified by aronwk-aaron to work better with LU-Toolbox
|
|
import bpy, bmesh
|
|
import mathutils
|
|
from bpy_extras.io_utils import (
|
|
ImportHelper,
|
|
axis_conversion,
|
|
)
|
|
|
|
import os
|
|
import sys
|
|
import math
|
|
import time
|
|
import struct
|
|
import zipfile
|
|
from xml.dom import minidom
|
|
import uuid
|
|
import random
|
|
import numpy as np
|
|
|
|
from .materials import (
|
|
MATERIALS_OPAQUE,
|
|
MATERIALS_TRANSPARENT,
|
|
MATERIALS_METALLIC,
|
|
MATERIALS_GLOW
|
|
)
|
|
|
|
from bpy.props import StringProperty, BoolProperty
|
|
from bpy.types import Operator, AddonPreferences
|
|
|
|
|
|
class ImportLDDPreferences(AddonPreferences):
|
|
bl_idname = __package__
|
|
brickdbpath: StringProperty(
|
|
name="Brick DB",
|
|
subtype='FILE_PATH'
|
|
)
|
|
|
|
def draw(self, context):
|
|
self.layout.label(text="Path to Brick DB (or luclient/res/)")
|
|
self.layout.prop(self, "brickdbpath")
|
|
|
|
|
|
class ImportLDDOps(Operator, ImportHelper):
|
|
"""This appears in the tooltip of the operator and in the generated docs"""
|
|
bl_description = "Import LEGO Digital Designer scenes (.lxf/.lxfml)"
|
|
bl_idname = "import_scene.importldd"
|
|
bl_label = "Import LDD scene"
|
|
|
|
# ImportHelper mixin class uses this
|
|
filename_ext = ".lxf"
|
|
|
|
filter_glob: StringProperty(
|
|
default="*.lxf;*.lxfml",
|
|
options={'HIDDEN'},
|
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
|
)
|
|
|
|
importLOD0: BoolProperty(
|
|
name="LOD0",
|
|
description="Import LOD0",
|
|
default=True,
|
|
)
|
|
|
|
importLOD1: BoolProperty(
|
|
name="LOD1",
|
|
description="Import LOD1",
|
|
default=False,
|
|
)
|
|
|
|
importLOD2: BoolProperty(
|
|
name="LOD2",
|
|
description="Import LOD2",
|
|
default=True,
|
|
)
|
|
|
|
importLOD3: BoolProperty(
|
|
name="LOD3",
|
|
description="Import LOD3",
|
|
default=True,
|
|
)
|
|
|
|
overwriteScene: BoolProperty(
|
|
name="Overwrite Scene",
|
|
description="Delete all objects and collections from Blender scene before importing.",
|
|
default=True,
|
|
)
|
|
|
|
useNormals: BoolProperty(
|
|
name="Use Normals",
|
|
description="Use normals when importing geometry",
|
|
default=True,
|
|
)
|
|
|
|
def execute(self, context):
|
|
return convertldd_data(
|
|
self,
|
|
context,
|
|
self.filepath,
|
|
self.importLOD0,
|
|
self.importLOD1,
|
|
self.importLOD2,
|
|
self.importLOD3,
|
|
self.overwriteScene,
|
|
self.useNormals
|
|
)
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(ImportLDDOps)
|
|
bpy.utils.register_class(ImportLDDPreferences)
|
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
|
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(ImportLDDOps)
|
|
bpy.utils.unregister_class(ImportLDDPreferences)
|
|
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
|
|
|
|
|
# Only needed if you want to add into a dynamic menu
|
|
def menu_func_import(self, context):
|
|
self.layout.operator(ImportLDDOps.bl_idname, text="LEGO Exchange Format (.lxf/.lxfml)")
|
|
|
|
|
|
def convertldd_data(self, context, filepath, importLOD0, importLOD1, importLOD2, importLOD3, overwriteScene, useNormals):
|
|
|
|
preferences = context.preferences
|
|
addon_prefs = preferences.addons[__package__].preferences
|
|
brickdbpath = addon_prefs.brickdbpath
|
|
|
|
primaryBrickDBPath = None
|
|
|
|
if brickdbpath:
|
|
primaryBrickDBPath = brickdbpath
|
|
else:
|
|
self.report({'ERROR'}, 'ERROR: Please define a Brick DB Path in the Addon Preferences')
|
|
return {'FINISHED'}
|
|
|
|
converter = Converter()
|
|
|
|
if os.path.isdir(primaryBrickDBPath):
|
|
self.report({'INFO'}, 'Found DB folder.')
|
|
start = time.process_time()
|
|
setDBFolderVars(dbfolderlocation=primaryBrickDBPath)
|
|
converter.LoadDBFolder(dbfolderlocation=primaryBrickDBPath)
|
|
end = time.process_time()
|
|
self.report({'INFO'}, f'Time taken to load Brick DB: {end - start} seconds')
|
|
|
|
try:
|
|
if overwriteScene:
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
|
|
bpy.ops.outliner.orphans_purge()
|
|
|
|
for c in context.scene.collection.children:
|
|
context.scene.collection.children.unlink(c)
|
|
|
|
converter.LoadScene(filename=filepath)
|
|
col = bpy.data.collections.new(converter.scene.Name)
|
|
bpy.context.scene.collection.children.link(col)
|
|
|
|
if importLOD0:
|
|
start = time.process_time()
|
|
converter.Export(
|
|
filename=filepath,
|
|
lod='0',
|
|
parent_collection=col,
|
|
useNormals=useNormals
|
|
)
|
|
end = time.process_time()
|
|
self.report({'INFO'}, f'Time taken to Load LOD0: {end - start} seconds')
|
|
if importLOD1:
|
|
start = time.process_time()
|
|
converter.Export(
|
|
filename=filepath,
|
|
lod='1',
|
|
parent_collection=col,
|
|
useNormals=useNormals
|
|
)
|
|
end = time.process_time()
|
|
self.report({'INFO'}, f'Time taken to Load LOD1: {end - start} seconds')
|
|
if importLOD2:
|
|
start = time.process_time()
|
|
converter.Export(
|
|
filename=filepath,
|
|
lod='2',
|
|
parent_collection=col,
|
|
useNormals=useNormals
|
|
)
|
|
end = time.process_time()
|
|
self.report({'INFO'}, f'Time taken to Load LOD2: {end - start} seconds')
|
|
LOD3_exists = False
|
|
if importLOD3:
|
|
for dirpath, dirnames, filenames in os.walk(primaryBrickDBPath):
|
|
for dirname in dirnames:
|
|
if dirname == "lod3":
|
|
LOD3_exists = True
|
|
if LOD3_exists:
|
|
start = time.process_time()
|
|
converter.Export(
|
|
filename=filepath,
|
|
lod='3',
|
|
parent_collection=col,
|
|
useNormals=useNormals
|
|
)
|
|
end = time.process_time()
|
|
self.report({'INFO'}, f'Time taken to Load LOD3: {end - start} seconds')
|
|
else:
|
|
self.report({'INFO'}, f'LOD3 does not exist, skipping')
|
|
except Exception as e:
|
|
self.report({'ERROR'}, str(e))
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
PRIMITIVEPATH = '/Primitives/'
|
|
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
|
|
|
|
|
|
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 f"[{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')
|
|
if node.hasAttribute('transformation'):
|
|
(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
|
|
)
|
|
elif node.hasAttribute('angle'):
|
|
# raise Exception("Cannot Properly import Old LDD Save formats")
|
|
rotationMatrix = Matrix3D()
|
|
rotationMatrix.rotate(
|
|
angle=float(node.getAttribute('angle')) * math.pi / 180.0,
|
|
axis=Point3D(
|
|
x=float(node.getAttribute('ax')),
|
|
y=float(node.getAttribute('ay')),
|
|
z=float(node.getAttribute('az'))
|
|
)
|
|
)
|
|
p = Point3D(
|
|
x=float(node.getAttribute('tx')),
|
|
y=float(node.getAttribute('ty')),
|
|
z=float(node.getAttribute('tz'))
|
|
)
|
|
p.transformW(rotationMatrix)
|
|
rotationMatrix.n41 = p.x
|
|
rotationMatrix.n42 = p.y
|
|
rotationMatrix.n43 = p.z
|
|
self.matrix = rotationMatrix
|
|
else:
|
|
raise Exception(f"Bone/Part {self.refID} transformation not supported")
|
|
|
|
|
|
class Part:
|
|
def __init__(self, node):
|
|
self.isGrouped = False
|
|
self.GroupIDX = 0
|
|
self.Bones = []
|
|
self.refID = node.getAttribute('refID')
|
|
self.designID = node.getAttribute('designID')
|
|
if node.hasAttribute('materials'):
|
|
self.materials = list(map(str, node.getAttribute('materials').split(',')))
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Bone':
|
|
self.Bones.append(Bone(node=childnode))
|
|
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
|
|
elif node.hasAttribute('materialID'):
|
|
self.materials = [str(node.getAttribute('materialID'))]
|
|
self.Bones.append(Bone(node=node))
|
|
else:
|
|
raise Exception("Not valid Part")
|
|
|
|
|
|
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 Scene:
|
|
def __init__(self, file):
|
|
self.Bricks = []
|
|
self.Groups = []
|
|
self.Version = "Unknown"
|
|
self.Name = "Unknown"
|
|
|
|
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)
|
|
try:
|
|
self.Name = xml.firstChild.getAttribute('name')
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
for node in xml.firstChild.childNodes:
|
|
if node.nodeName == 'Meta':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'BrickSet':
|
|
try:
|
|
self.Version = str(childnode.getAttribute('version'))
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
elif node.nodeName == 'Bricks':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Brick':
|
|
self.Bricks.append(Brick(node=childnode))
|
|
elif node.nodeName == 'Scene':
|
|
for childnode in node.childNodes:
|
|
if childnode.nodeName == 'Model':
|
|
for childnode2 in childnode.childNodes:
|
|
if childnode2.nodeName == 'Group':
|
|
self.Bricks.append(Brick(node=childnode2))
|
|
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
|
|
|
|
|
|
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, lod):
|
|
self.designID = designID
|
|
self.Parts = {}
|
|
self.maxGeoBounding = -1
|
|
|
|
if lod is None:
|
|
geompath = GEOMETRIEPATH
|
|
else:
|
|
geompath = os.path.join(database.location, 'brickprimitives', 'lod' + lod + '/')
|
|
|
|
GeometryLocation = os.path.normpath('{0}{1}{2}'.format(geompath, designID, '.g'))
|
|
GeometryCount = 0
|
|
while str(GeometryLocation) in database.filelist:
|
|
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
|
|
GeometryCount += 1
|
|
GeometryLocation = os.path.normpath(f'{geompath}{designID}.g{GeometryCount}')
|
|
|
|
primitive = Primitive(data=database.filelist[os.path.normpath(PRIMITIVEPATH + designID + '.xml')].read())
|
|
self.Partname = primitive.Designname
|
|
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(f'\nBounding errror in part {designID}: {e}\n')
|
|
|
|
# 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 f'[type="{self.type}" transform="{self.matrix}" custom2DField="{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 f'[0,0,0] \
|
|
[{self.corner.x},0,0] \
|
|
[0,{self.corner.y},0] \
|
|
[{self.corner.x},{self.corner.y},0] \
|
|
[0,0,{self.corner.z}] \
|
|
[0,{self.corner.y},{self.corner.z}] \
|
|
[{self.corner.x},0,{2}] \
|
|
[{self.corner.x},{1},{2}]'
|
|
|
|
|
|
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)
|
|
)
|
|
)
|
|
|
|
|
|
class Materials:
|
|
def __init__(self):
|
|
self.MaterialsRi = {}
|
|
self.loadColors(MATERIALS_OPAQUE, "shinyPlastic")
|
|
self.loadColors(MATERIALS_TRANSPARENT, "Transparent")
|
|
self.loadColors(MATERIALS_METALLIC, "Metallic")
|
|
self.loadColors(MATERIALS_GLOW, "Glow")
|
|
|
|
def loadColors(self, data, materialType):
|
|
for color in data:
|
|
if color not in self.MaterialsRi:
|
|
self.MaterialsRi[color] = MaterialRi(
|
|
materialId=color,
|
|
r=data[color][0],
|
|
g=data[color][1],
|
|
b=data[color][2],
|
|
a=data[color][3],
|
|
materialType=materialType
|
|
)
|
|
|
|
def getMaterialRibyId(self, mid):
|
|
if mid in self.MaterialsRi:
|
|
return self.MaterialsRi[mid]
|
|
else:
|
|
print(f"Material {mid} does not exist")
|
|
return self.MaterialsRi["26"]
|
|
|
|
|
|
class MaterialRi:
|
|
def __init__(self, materialId, r, g, b, a, materialType):
|
|
self.materialType = materialType
|
|
self.materialId = materialId
|
|
self.r = r
|
|
self.g = g
|
|
self.b = b
|
|
self.a = a
|
|
|
|
def string(self, decorationId):
|
|
|
|
matId_or_decId = self.materialId
|
|
|
|
material = bpy.data.materials.new(matId_or_decId)
|
|
material.diffuse_color = (self.r, self.g, self.b, self.a)
|
|
|
|
return material
|
|
|
|
|
|
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 DBFolderReader:
|
|
def __init__(self, folder):
|
|
self.filelist = {}
|
|
self.initok = False
|
|
self.location = folder
|
|
|
|
try:
|
|
os.path.isdir(self.location)
|
|
except Exception:
|
|
self.initok = False
|
|
print("db folder read FAIL")
|
|
return
|
|
else:
|
|
self.parse()
|
|
if len(self.filelist) > 1:
|
|
print("DB folder OK.")
|
|
self.initok = True
|
|
else:
|
|
raise Exception("DB folder ERROR")
|
|
|
|
def fileexist(self, filename):
|
|
return filename in self.filelist
|
|
|
|
def parse(self):
|
|
if not os.path.exists(os.path.join(self.location, "Assemblies")) and \
|
|
os.path.exists(os.path.join(self.location, "brickdb.zip")):
|
|
print("Found brickdb.zip without uzipped files")
|
|
with zipfile.ZipFile(os.path.join(self.location, "brickdb.zip"), 'r') as zip_ref:
|
|
print("Extracting brickdb.zip")
|
|
zip_ref.extractall(self.location)
|
|
extentions = ('.g', '.g1', '.g2', '.g3', '.g4', '.xml')
|
|
for path, subdirs, files in os.walk(self.location):
|
|
files = filter(lambda file: file.endswith(extentions), files)
|
|
for name in files:
|
|
entryName = os.path.join(path, name)
|
|
self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName)
|
|
|
|
|
|
class Converter:
|
|
|
|
def LoadDBFolder(self, dbfolderlocation):
|
|
self.database = DBFolderReader(folder=dbfolderlocation)
|
|
|
|
if self.database.initok:
|
|
self.allMaterials = Materials()
|
|
|
|
def LoadScene(self, filename):
|
|
if self.database.initok:
|
|
self.scene = Scene(file=filename)
|
|
|
|
def Export(self, filename, lod=None, parent_collection=None, useNormals=True):
|
|
invert = Matrix3D()
|
|
usedmaterials = []
|
|
geometriecache = {}
|
|
current = 0
|
|
currentpart = 0
|
|
|
|
miny = 1000
|
|
|
|
global_matrix = axis_conversion(from_forward='-Z', from_up='Y', to_forward='Y', to_up='Z').to_4x4()
|
|
if lod is not None:
|
|
col = bpy.data.collections.new(self.scene.Name + '_LOD_' + lod)
|
|
else:
|
|
col = bpy.data.collections.new(self.scene.Name)
|
|
|
|
if parent_collection:
|
|
parent_collection.children.link(col)
|
|
else:
|
|
bpy.context.scene.collection.children.link(col)
|
|
|
|
for bri in self.scene.Bricks:
|
|
current += 1
|
|
|
|
for pa in bri.Parts:
|
|
currentpart += 1
|
|
try:
|
|
if pa.designID not in geometriecache:
|
|
geo = Geometry(designID=pa.designID, database=self.database, lod=lod)
|
|
geometriecache[pa.designID] = geo
|
|
else:
|
|
geo = geometriecache[pa.designID]
|
|
except Exception:
|
|
print(f'WARNING: Missing geo for {pa.designID}')
|
|
continue
|
|
|
|
# Read out 1st Bone matrix values
|
|
ind = 0
|
|
n11 = pa.Bones[ind].matrix.n11
|
|
n12 = pa.Bones[ind].matrix.n12
|
|
n13 = pa.Bones[ind].matrix.n13
|
|
n14 = pa.Bones[ind].matrix.n14
|
|
n21 = pa.Bones[ind].matrix.n21
|
|
n22 = pa.Bones[ind].matrix.n22
|
|
n23 = pa.Bones[ind].matrix.n23
|
|
n24 = pa.Bones[ind].matrix.n24
|
|
n31 = pa.Bones[ind].matrix.n31
|
|
n32 = pa.Bones[ind].matrix.n32
|
|
n33 = pa.Bones[ind].matrix.n33
|
|
n34 = pa.Bones[ind].matrix.n34
|
|
n41 = pa.Bones[ind].matrix.n41
|
|
n42 = pa.Bones[ind].matrix.n42
|
|
n43 = pa.Bones[ind].matrix.n43
|
|
n44 = pa.Bones[ind].matrix.n44
|
|
|
|
# Only parts with more then 1 bone are flex parts and for these we need to undo the transformation later
|
|
flexflag = 1
|
|
uniqueId = str(uuid.uuid4().hex)
|
|
material_string = '_' + '_'.join(pa.materials)
|
|
written_obj = geo.designID + material_string
|
|
brick_name = f"brick_{currentpart}_{written_obj}"
|
|
|
|
if (len(pa.Bones) > flexflag):
|
|
# Flex parts are "unique". Ensure they get a unique filename
|
|
written_obj = written_obj + "_" + uniqueId
|
|
part_matrix = global_matrix
|
|
else:
|
|
# Flex parts don't need to be moved, but non-flex parts need
|
|
transform_matrix = mathutils.Matrix(
|
|
(
|
|
(n11, n21, n31, n41),
|
|
(n12, n22, n32, n42),
|
|
(n13, n23, n33, n43),
|
|
(n14, n24, n34, n44)
|
|
)
|
|
)
|
|
|
|
# Random Scale for brick seams
|
|
scalefact = (geo.maxGeoBounding - 0.000 * random.uniform(0.0, 1.000)) / geo.maxGeoBounding
|
|
|
|
scale_matrix = mathutils.Matrix.Scale(scalefact, 4)
|
|
part_matrix = global_matrix @ transform_matrix @ scale_matrix
|
|
|
|
# miny used for floor plane later
|
|
if miny > float(n42):
|
|
miny = n42
|
|
|
|
last_color = 0
|
|
geo_meshes = []
|
|
for part in geo.Parts:
|
|
|
|
written_geo = str(geo.designID) + '_' + str(part)
|
|
|
|
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]
|
|
|
|
# translate / rotate only parts with more then 1 bone. This are flex parts
|
|
if (len(pa.Bones) > flexflag):
|
|
|
|
written_geo = written_geo + '_' + uniqueId
|
|
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)
|
|
|
|
if "geo{0}".format(written_geo) not in geometriecache:
|
|
|
|
mesh = bpy.data.meshes.new("geo{0}".format(written_geo))
|
|
|
|
verts = []
|
|
for point in geo.Parts[part].outpositions:
|
|
single_vert = mathutils.Vector([point.x, point.y, point.z])
|
|
verts.append(single_vert)
|
|
|
|
normals = []
|
|
for point in geo.Parts[part].outnormals:
|
|
single_norm = mathutils.Vector([point.x, point.y, point.z])
|
|
normals.append(single_norm)
|
|
|
|
faces = []
|
|
for face in geo.Parts[part].faces:
|
|
single_face = [face.a, face.b, face.c]
|
|
faces.append(single_face)
|
|
|
|
edges = []
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
for f in mesh.polygons:
|
|
f.use_smooth = True
|
|
|
|
if useNormals:
|
|
mesh.calc_normals_split()
|
|
mesh.normals_split_custom_set_from_vertices(normals)
|
|
mesh.use_auto_smooth = True
|
|
|
|
geometriecache["geo{0}".format(written_geo)] = mesh.copy()
|
|
|
|
else:
|
|
mesh = geometriecache["geo{0}".format(written_geo)].copy()
|
|
mesh.materials.clear()
|
|
|
|
geo_meshes.append(mesh)
|
|
|
|
# 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:
|
|
materialCurrentPart = last_color
|
|
|
|
lddmatri = self.allMaterials.getMaterialRibyId(materialCurrentPart)
|
|
matname = materialCurrentPart
|
|
|
|
if matname not in usedmaterials:
|
|
mesh.materials.append(lddmatri.string(None))
|
|
|
|
if len(geo.Parts[part].textures) > 0:
|
|
|
|
mesh.uv_layers.new(do_init=False)
|
|
uv_layer = mesh.uv_layers.active.data
|
|
|
|
uvs = []
|
|
for text in geo.Parts[part].textures:
|
|
uv = [text.x, (-1) * text.y]
|
|
uvs.append(uv)
|
|
for poly in mesh.polygons:
|
|
# range is used here to show how the polygons reference loops,
|
|
# for convenience 'poly.loop_indices' can be used instead.
|
|
for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
|
|
uv_layer[loop_index].uv = uvs[mesh.loops[loop_index].vertex_index]
|
|
|
|
used_materials = []
|
|
used_material_indices = {}
|
|
bm = bmesh.new()
|
|
for mesh in geo_meshes:
|
|
index_remapping = np.empty(len(mesh.materials), dtype=int)
|
|
for i, material in enumerate(mesh.materials):
|
|
mat_name = material.name.rsplit(".", 1)[0]
|
|
if (index := used_material_indices.get(mat_name)) is not None:
|
|
index_remapping[i] = index
|
|
bpy.data.materials.remove(material)
|
|
else:
|
|
index_remapping[i] = len(used_material_indices)
|
|
used_material_indices[mat_name] = index_remapping[i]
|
|
used_materials.append(material)
|
|
|
|
material_indices = np.empty(len(mesh.polygons), dtype=int)
|
|
mesh.polygons.foreach_get("material_index", material_indices)
|
|
remapped_indices = index_remapping[material_indices]
|
|
mesh.polygons.foreach_set("material_index", remapped_indices)
|
|
|
|
bm.from_mesh(mesh)
|
|
bpy.data.meshes.remove(mesh)
|
|
|
|
brick_mesh = bpy.data.meshes.new(brick_name)
|
|
bm.to_mesh(brick_mesh)
|
|
for material in used_materials:
|
|
brick_mesh.materials.append(material)
|
|
|
|
if useNormals:
|
|
brick_mesh.use_auto_smooth = True
|
|
|
|
brick_obj = bpy.data.objects.new(brick_name, brick_mesh)
|
|
brick_obj.matrix_world = part_matrix
|
|
col.objects.link(brick_obj)
|
|
|
|
for mesh in geometriecache.values():
|
|
if type(mesh) == bpy.types.Mesh:
|
|
bpy.data.meshes.remove(mesh)
|
|
|
|
useplane = True
|
|
if useplane is True: # write the floor plane in case True
|
|
i = 0
|
|
|
|
|
|
def setDBFolderVars(dbfolderlocation):
|
|
global PRIMITIVEPATH
|
|
global GEOMETRIEPATH
|
|
global MATERIALNAMESPATH
|
|
PRIMITIVEPATH = dbfolderlocation + '/Primitives/'
|
|
GEOMETRIEPATH = dbfolderlocation + '/Primitives/LOD0/'
|
|
MATERIALNAMESPATH = dbfolderlocation + '/MaterialNames/'
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|
|
|
|
# test call
|
|
bpy.ops.import_scene.importldd('INVOKE_DEFAULT')
|