|
1 import datetime |
|
2 import parse |
|
3 import os |
|
4 from pathlib import Path |
|
5 import linetypes |
|
6 import math |
|
7 |
|
8 def find_ldraw_file(filename, libraries): |
|
9 ''' |
|
10 Searches the LDraw libraries for the specified file. |
|
11 `libraries` must be an iterable of LDraw library root paths. |
|
12 ''' |
|
13 if os.path.sep != "\\": |
|
14 filename = filename.replace("\\", os.path.sep) |
|
15 for library in libraries: |
|
16 for path in [ |
|
17 Path(library) / filename, |
|
18 Path(library) / "parts" / filename, |
|
19 Path(library) / "p" / filename, |
|
20 ]: |
|
21 if path.exists(): |
|
22 return path |
|
23 raise FileNotFoundError(str.format( |
|
24 '"{}" not found in the LDraw libraries', |
|
25 filename, |
|
26 )) |
|
27 |
|
28 class CyclicalReferenceError(Exception): |
|
29 pass |
|
30 |
|
31 class SubfileCache: |
|
32 class Subfile: |
|
33 def __init__(self): |
|
34 self.valid = None |
|
35 self.flatness = None |
|
36 self.description = None |
|
37 self.problem = None |
|
38 self.vertices = set() |
|
39 def __init__(self, ldraw_directories): |
|
40 ''' |
|
41 Initializes a new subfile cache |
|
42 ''' |
|
43 self.cache = dict() |
|
44 self.ldraw_directories = [ |
|
45 Path(os.path.expanduser(directory)) |
|
46 for directory in ldraw_directories |
|
47 ] |
|
48 self.reference_stack = [] |
|
49 def flatness_of(self, filename): |
|
50 ''' |
|
51 Returns the set of all directiones the specified file is flat in. |
|
52 ''' |
|
53 self.prepare_file(filename) |
|
54 return self.cache[filename].flatness |
|
55 def description_of(self, filename): |
|
56 ''' |
|
57 Returns the description of the specified file |
|
58 ''' |
|
59 self.prepare_file(filename) |
|
60 return self.cache[filename].description |
|
61 def find_file(self, filename): |
|
62 return find_ldraw_file( |
|
63 filename = filename, |
|
64 libraries = self.ldraw_directories, |
|
65 ) |
|
66 def prepare_file(self, filename): |
|
67 ''' |
|
68 Loads the file if not loaded yet. |
|
69 ''' |
|
70 if filename not in self.cache: |
|
71 self._load_subfile(filename) |
|
72 return self.cache[filename] |
|
73 def _load_subfile(self, filename): |
|
74 # ward against cyclical dependencies |
|
75 if filename in self.reference_stack: |
|
76 raise CyclicalReferenceError( |
|
77 ' -> '.join(self.reference_stack + [filename]), |
|
78 ) |
|
79 self.reference_stack.append(filename) |
|
80 subfile = SubfileCache.Subfile() |
|
81 self.cache[filename] = subfile |
|
82 try: |
|
83 path = self.find_file(filename) |
|
84 with path.open() as file: |
|
85 model = parse.read_ldraw( |
|
86 file, |
|
87 ldraw_directories = self.ldraw_directories, |
|
88 ) |
|
89 except (FileNotFoundError, IOError, PermissionError) as error: |
|
90 subfile.valid = False |
|
91 subfile.problem = str(error) |
|
92 else: |
|
93 if isinstance(model.body[0], linetypes.MetaCommand): |
|
94 subfile.valid = True |
|
95 subfile.description = model.body[0].text |
|
96 else: |
|
97 subfile.valid = False |
|
98 subfile.problem = 'Description not found' |
|
99 if subfile.valid: |
|
100 subfile.vertices = set(parse.model_vertices(model)) |
|
101 subfile.flatness = {'x', 'y', 'z'} |
|
102 for vertex in subfile.vertices: |
|
103 # Use list(subfile.flatness) for iteration because the |
|
104 # actual set may be modified during the loop |
|
105 for dimension in list(subfile.flatness): |
|
106 if not math.isclose( |
|
107 getattr(vertex, dimension), |
|
108 0.0, |
|
109 abs_tol = 1e-05, |
|
110 ): |
|
111 subfile.flatness.remove(dimension) |
|
112 if not subfile.flatness: |
|
113 break |
|
114 for subfile_reference in model.subfile_references: |
|
115 # Go through all the subfile references so that we catch any |
|
116 # cyclical references |
|
117 self.prepare_file(subfile_reference.subfile_path) |
|
118 self.reference_stack.pop() |
|
119 |
|
120 if __name__ == '__main__': |
|
121 cache = SubfileCache(ldraw_directories = ["~/ldraw"]) |