forked from le717/LDR-Importer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
import_ldraw.py
589 lines (478 loc) · 21.1 KB
/
import_ldraw.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# -*- coding: utf-8 -*-
"""LDR Importer GPLv2 license.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
import os
import math
import mathutils
import traceback
import bpy
from bpy_extras.io_utils import ImportHelper
from .src.ldcolors import Colors
from .src.ldconsole import Console
from .src.ldmaterials import Materials
from .src.ldprefs import Preferences
from .src.extras import cleanup as Extra_Cleanup
from .src.extras import gaps as Extra_Part_Gaps
from .src.extras import linked_parts as Extra_Part_Linked
# Global variables
objects = []
paths = []
class LDrawFile(object):
"""Scans LDraw files."""
# FIXME: rewrite - Rewrite entire class (#35)
def __init__(self, context, filename, level, mat,
colour=None, orientation=None):
self.level = level
self.points = []
self.faces = []
self.material_index = []
self.subparts = []
self.submodels = []
self.part_count = 0
# Orientation matrix to handle orientation separately
# (top-level part only)
self.orientation = orientation
self.mat = mat
self.colour = colour
self.parse(filename)
# Deselect all objects before import.
# This prevents them from receiving any cleanup (if applicable).
bpy.ops.object.select_all(action='DESELECT')
if len(self.points) > 0 and len(self.faces) > 0:
mesh = bpy.data.meshes.new("LDrawMesh")
mesh.from_pydata(self.points, [], self.faces)
mesh.validate()
mesh.update()
for i, f in enumerate(mesh.polygons):
n = self.material_index[i]
# Get the material depending on the current render engine
material = ldMaterials.make(n)
if material is not None:
if mesh.materials.get(material.name) is None:
mesh.materials.append(material)
f.material_index = mesh.materials.find(material.name)
# Naming of objects: filename of .dat-file, without extension
self.ob = bpy.data.objects.new("LDrawObj", mesh)
self.ob.name = os.path.basename(filename)[:-4]
if LinkParts: # noqa
# Set top-level part orientation using Blender's 'matrix_world'
self.ob.matrix_world = self.orientation.normalized()
else:
self.ob.location = (0, 0, 0)
objects.append(self.ob)
# Link object to scene
bpy.context.scene.objects.link(self.ob)
for i in self.subparts:
self.submodels.append(LDrawFile(context, i[0], i[1], i[2],
i[3], i[4]))
def parse_line(self, line):
"""Harvest the information from each line."""
verts = []
color = line[1]
if color == '16':
color = self.colour
num_points = int((len(line) - 2) / 3)
for i in range(num_points):
self.points.append(
(self.mat * mathutils.Vector((float(line[i * 3 + 2]),
float(line[i * 3 + 3]), float(line[i * 3 + 4])))).
to_tuple())
verts.append(len(self.points) - 1)
self.faces.append(verts)
self.material_index.append(color)
def parse_quad(self, line):
"""Properly construct quads in each brick."""
color = line[1]
verts = []
num_points = 4
v = []
if color == '16':
color = self.colour
v.append(self.mat * mathutils.Vector((float(line[0 * 3 + 2]),
float(line[0 * 3 + 3]), float(line[0 * 3 + 4]))))
v.append(self.mat * mathutils.Vector((float(line[1 * 3 + 2]),
float(line[1 * 3 + 3]), float(line[1 * 3 + 4]))))
v.append(self.mat * mathutils.Vector((float(line[2 * 3 + 2]),
float(line[2 * 3 + 3]), float(line[2 * 3 + 4]))))
v.append(self.mat * mathutils.Vector((float(line[3 * 3 + 2]),
float(line[3 * 3 + 3]), float(line[3 * 3 + 4]))))
nA = (v[1] - v[0]).cross(v[2] - v[0])
nB = (v[2] - v[1]).cross(v[3] - v[1])
for i in range(num_points):
verts.append(len(self.points) + i)
if nA.dot(nB) < 0:
self.points.extend([v[0].to_tuple(), v[1].to_tuple(),
v[3].to_tuple(), v[2].to_tuple()])
else:
self.points.extend([v[0].to_tuple(), v[1].to_tuple(),
v[2].to_tuple(), v[3].to_tuple()])
self.faces.append(verts)
self.material_index.append(color)
def parse(self, filename):
"""Construct tri's in each brick."""
# FIXME: rewrite - Rework function (#35)
subfiles = []
while True:
# Get the path to the part
filename = (filename if os.path.exists(filename)
else locatePart(filename))
# The part does not exist
# TODO Do not halt on this condition (#11)
if filename is None:
return False
# Read the located part
with open(filename, "rt", encoding="utf_8") as f:
lines = f.readlines()
# Some models may not have headers or enough lines
# to support a header. Handle this case to avoid
# hitting an IndexError trying to extract the header line.
partTypeLine = ("" if len(lines) <= 3 else lines[3])
# Check the part header for top-level part status
is_top_part = is_top_level_part(partTypeLine)
# Linked parts relies on the flawed is_top_part logic (#112)
# TODO Correct linked parts to use proper logic
# and remove this kludge
if LinkParts: # noqa
is_top_part = filename == fileName # noqa
self.part_count += 1
if self.part_count > 1 and self.level == 0:
self.subparts.append([filename, self.level + 1, self.mat,
self.colour, self.orientation])
else:
for retval in lines:
tmpdate = retval.strip()
if tmpdate != "":
tmpdate = tmpdate.split()
# Part content
if tmpdate[0] == "1":
new_file = tmpdate[14]
(
x, y, z, a, b, c,
d, e, f, g, h, i
) = map(float, tmpdate[2:14])
# Reset orientation of top-level part,
# track original orientation
# TODO Use corrected isPart logic
if self.part_count == 1 and is_top_part and LinkParts: # noqa
mat_new = self.mat * mathutils.Matrix((
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0),
(0, 0, 0, 1)
))
orientation = self.mat * mathutils.Matrix((
(a, b, c, x),
(d, e, f, y),
(g, h, i, z),
(0, 0, 0, 1)
)) * mathutils.Matrix.Rotation(
math.radians(90), 4, 'X')
else:
mat_new = self.mat * mathutils.Matrix((
(a, b, c, x),
(d, e, f, y),
(g, h, i, z),
(0, 0, 0, 1)
))
orientation = None
color = tmpdate[1]
if color == '16':
color = self.colour
subfiles.append([new_file, mat_new, color])
# When top-level part, save orientation separately
# TODO Use corrected is_top_part logic
if self.part_count == 1 and is_top_part:
subfiles.append(['orientation',
orientation, ''])
# Triangle (tri)
if tmpdate[0] == "3":
self.parse_line(tmpdate)
# Quadrilateral (quad)
if tmpdate[0] == "4":
self.parse_quad(tmpdate)
if len(subfiles) > 0:
subfile = subfiles.pop()
filename = subfile[0]
# When top-level brick orientation information found,
# save it in self.orientation
if filename == 'orientation':
self.orientation = subfile[1]
subfile = subfiles.pop()
filename = subfile[0]
self.mat = subfile[1]
self.colour = subfile[2]
else:
break
def is_top_level_part(header_line):
"""Check if the given part is a top level part.
@param {String} headerLine The header line stating the part level.
@return {Boolean} True if a top level part, False otherwise
or the header does not specify.
"""
# Make sure the file has the spec'd META command
# If it does not, we cannot do easily determine the part type,
# so we will simply say it is not top level
header_line = header_line.lower().strip()
if header_line == "":
return False
header_line = header_line.split()
if header_line[0] != "0 !ldraw_org":
return False
# We can determine if this is top level or not
return header_line[2] in ("part", "unofficial_part")
def locatePart(partName):
"""Find the given part in the defined search paths.
@param {String} partName The part to find.
@return {!String} The absolute path to the part if found.
"""
# Use the OS's path separator to ensure the parts are found
partName = partName.replace("\\", os.path.sep)
for path in paths:
# Find the part filename using the exact case in the file
fname = os.path.join(path, partName)
if os.path.exists(fname):
return fname
# Because case-sensitive file systems, if the first check fails
# check again using a normalized part filename
# See #112#issuecomment-136719763
else:
fname = os.path.join(path, partName.lower())
if os.path.exists(fname):
return fname
Console.log("Could not find part {0}".format(fname))
return None
def create_model(self, context, scale):
"""Create the actual model."""
# FIXME: rewrite - Rewrite entire function (#35)
global objects
global ldColors
global ldMaterials
global fileName
fileName = self.filepath
# Attempt to get the directory the file came from
# and add it to the `paths` list
paths[0] = os.path.dirname(fileName)
Console.log("Attempting to import {0}".format(fileName))
# The file format as hinted to by
# conventional file extensions is not supported.
# Recommended: http://ghost.kirk.by/file-extensions-are-only-hints
if fileName[-4:].lower() not in (".ldr", ".dat"):
Console.log('''ERROR: Reason: Invalid File Type
Must be a .ldr or .dat''')
self.report({'ERROR'}, '''Error: Invalid File Type
Must be a .ldr or .dat''')
return {'ERROR'}
# It has the proper file extension, continue with the import
try:
# Rotate and scale the parts
# Scale factor is divided by 25 so we can use whole number
# scale factors in the UI. For reference,
# the default scale 1 = 0.04 to Blender
trix = mathutils.Matrix((
(1.0, 0.0, 0.0, 0.0), # noqa
(0.0, 0.0, 1.0, 0.0), # noqa
(0.0, -1.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0) # noqa
)) * (scale / 25)
# If LDrawDir does not exist, stop the import
if not os.path.isdir(LDrawDir): # noqa
Console.log(''''ERROR: Cannot find LDraw installation at
{0}'''.format(LDrawDir)) # noqa
self.report({'ERROR'}, '''Cannot find LDraw installation at
{0}'''.format(LDrawDir)) # noqa
return {'CANCELLED'}
# Instance the colors module and
# load the LDraw-defined color definitions
ldColors = Colors(LDrawDir, AltColorsOpt) # noqa
ldColors.load()
ldMaterials = Materials(ldColors, context.scene.render.engine)
LDrawFile(context, fileName, 0, trix)
for cur_obj in objects:
# The CleanUp import option was selected
if CleanUpOpt: # noqa
Extra_Cleanup.main(cur_obj, LinkParts) # noqa
if GapsOpt: # noqa
Extra_Part_Gaps.main(cur_obj, scale)
# The link identical parts import option was selected
if LinkParts: # noqa
Extra_Part_Linked.main(objects)
# Select all the mesh now that import is complete
for cur_obj in objects:
cur_obj.select = True
# Update the scene with the changes
context.scene.update()
objects = []
# Always reset 3D cursor to <0,0,0> after import
bpy.context.scene.cursor_location = (0.0, 0.0, 0.0)
# Display success message
Console.log("{0} successfully imported!".format(fileName))
return {'FINISHED'}
except Exception as e:
Console.log("ERROR: {0}\n{1}\n".format(
type(e).__name__, traceback.format_exc()))
Console.log("ERROR: Reason: {0}.".format(
type(e).__name__))
self.report({'ERROR'}, '''File not imported ("{0}").
Check the console logs for more information.'''.format(type(e).__name__))
return {'CANCELLED'}
# ------------ Operator ------------ #
class LDRImporterOps(bpy.types.Operator, ImportHelper):
"""LDR Importer Import Operator."""
bl_idname = "import_scene.ldraw"
bl_description = "Import an LDraw model (.ldr/.dat)"
bl_label = "Import LDraw Model"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
# Instance the preferences system
prefs = Preferences()
# File type filter in file browser
filename_ext = ".ldr"
filter_glob = bpy.props.StringProperty(
default="*.ldr;*.dat",
options={'HIDDEN'}
)
ldrawPath = bpy.props.StringProperty(
name="",
description="Path to the LDraw Parts Library",
default=prefs.getLDraw()
)
importScale = bpy.props.FloatProperty(
name="Scale",
description="Use a specific scale for each part",
default=prefs.get("importScale", 1.00)
)
resPrims = bpy.props.EnumProperty(
name="Resolution of part primitives",
description="Resolution of part primitives",
default=prefs.get("resPrims", "StandardRes"),
items=(
("HighRes", "High-Res Primitives",
"Import using high resolution primitives. "
"NOTE: This feature may create mesh errors"),
("StandardRes", "Standard Primitives",
"Import using standard resolution primitives"),
("LowRes", "Low-Res Primitives",
"Import using low resolution primitives. "
"NOTE: This feature may create mesh errors")
)
)
cleanUpParts = bpy.props.BoolProperty(
name="Model Cleanup",
description="Perform some basic model cleanup",
default=prefs.get("cleanUpParts", True)
)
altColors = bpy.props.BoolProperty(
name="Use Alternate Colors",
description="Use LDCfgalt.ldr for color definitions",
default=prefs.get("altColors", False)
)
addGaps = bpy.props.BoolProperty(
name="Spaces Between Parts",
description="Add small spaces between each part",
default=prefs.get("addGaps", False)
)
lsynthParts = bpy.props.BoolProperty(
name="Use LSynth Parts",
description="Use LSynth parts during import",
default=prefs.get("lsynthParts", False)
)
linkParts = bpy.props.BoolProperty(
name="Link Identical Parts",
description="Link identical parts by type and color (experimental)",
default=prefs.get("linkParts", False)
)
def draw(self, context):
"""Display import options."""
layout = self.layout
box = layout.box()
box.label("Import Options", icon="SCRIPTWIN")
box.label("LDraw Parts Library", icon="FILESEL")
box.prop(self, "ldrawPath")
box.prop(self, "importScale")
box.label("Primitives", icon="MOD_BUILD")
box.prop(self, "resPrims", expand=True)
box.label("Additional Options", icon="PREFERENCES")
box.prop(self, "linkParts")
box.prop(self, "cleanUpParts", expand=True)
box.prop(self, "addGaps")
box.prop(self, "altColors")
box.prop(self, "lsynthParts")
def execute(self, context):
"""Set import options and start the import process."""
global LDrawDir, CleanUpOpt, AltColorsOpt, GapsOpt, LinkParts
LDrawDir = str(self.ldrawPath)
CleanUpOpt = bool(self.cleanUpParts)
AltColorsOpt = bool(self.altColors)
GapsOpt = bool(self.addGaps)
LinkParts = bool(self.linkParts)
# Clear array before adding data if it contains data already
# Not doing so duplicates the indexes
if paths:
del paths[:]
# Create placeholder for index 0.
# It will be filled with the location of the model later.
paths.append("")
# Always search for parts in the `models` folder
paths.append(os.path.join(self.ldrawPath, "models"))
# The unofficial folder exists, search the standard folders
if os.path.exists(os.path.join(self.ldrawPath, "unofficial")):
paths.append(os.path.join(self.ldrawPath, "unofficial", "parts"))
# The user wants to use high-res unofficial primitives
if self.resPrims == "HighRes":
paths.append(os.path.join(self.ldrawPath,
"unofficial", "p", "48"))
# The user wants to use low-res unofficial primitives
elif self.resPrims == "LowRes":
paths.append(os.path.join(self.ldrawPath,
"unofficial", "p", "8"))
# Search in the `unofficial/p` folder
paths.append(os.path.join(self.ldrawPath, "unofficial", "p"))
# The user wants to use LSynth parts
if self.lsynthParts:
if os.path.exists(os.path.join(self.ldrawPath, "unofficial",
"lsynth")):
paths.append(os.path.join(self.ldrawPath, "unofficial",
"lsynth"))
Console.log("Use LSynth Parts selected")
# Always search for parts in the `parts` folder
paths.append(os.path.join(self.ldrawPath, "parts"))
# The user wants to use high-res primitives
if self.resPrims == "HighRes":
paths.append(os.path.join(self.ldrawPath, "p", "48"))
Console.log("High-res primitives substitution selected")
# The user wants to use low-res primitives
elif self.resPrims == "LowRes":
paths.append(os.path.join(self.ldrawPath, "p", "8"))
Console.log("Low-res primitives substitution selected")
# The user wants to use normal-res primitives
else:
Console.log("Standard-res primitives substitution selected")
# Finally, search in the `p` folder
paths.append(os.path.join(self.ldrawPath, "p"))
# Create the preferences dictionary
importOpts = {
"addGaps": self.addGaps,
"altColors": self.altColors,
"cleanUpParts": self.cleanUpParts,
"importScale": self.importScale,
"linkParts": self.linkParts,
"lsynthParts": self.lsynthParts,
"resPrims": self.resPrims
}
# Save the preferences and import the model
self.prefs.setLDraw(self.ldrawPath)
self.prefs.save(importOpts)
create_model(self, context, self.importScale)
return {'FINISHED'}