Tue, 25 Aug 2020 23:04:27 +0300
replaced the collinear test with a new one based on the hairline test that checks interior angles against official limits of 0.025 and 179.9
import datetime import parse import os from pathlib import Path import linetypes import math def find_ldraw_file(filename, libraries): ''' Searches the LDraw libraries for the specified file. `libraries` must be an iterable of LDraw library root paths. ''' if os.path.sep != "\\": filename = filename.replace("\\", os.path.sep) for library in libraries: for path in [ Path(library) / filename, Path(library) / "parts" / filename, Path(library) / "p" / filename, ]: if path.exists(): return path raise FileNotFoundError(str.format( '"{}" not found in the LDraw libraries', filename, )) def is_logo_stud_name(name): return name in ['stud.dat', 'stud3.dat'] class CyclicalReferenceError(Exception): pass class SubfileCache: class Subfile: def __init__(self): self.valid = None self.flatness = None self.description = None self.problem = None self.has_studs = None # Whether or not it contains studs self.vertices = set() def __init__(self, ldraw_directories): ''' Initializes a new subfile cache ''' self.cache = dict() if ldraw_directories and isinstance(ldraw_directories[0], str): self.ldraw_directories = [ Path(os.path.expanduser(directory)) for directory in ldraw_directories ] else: from copy import copy self.ldraw_directories = copy(ldraw_directories) self.reference_stack = [] def flatness_of(self, filename): ''' Returns the set of all directiones the specified file is flat in. ''' self.prepare_file(filename) return self.cache[filename].flatness def description_of(self, filename): ''' Returns the description of the specified file ''' self.prepare_file(filename) return self.cache[filename].description def find_file(self, filename): return find_ldraw_file( filename = filename, libraries = self.ldraw_directories, ) def prepare_file(self, filename): ''' Loads the file if not loaded yet. ''' if filename not in self.cache: self._load_subfile(filename) return self.cache[filename] def _load_subfile(self, filename): # ward against cyclical dependencies if filename in self.reference_stack: raise CyclicalReferenceError( ' -> '.join(self.reference_stack + [filename]), ) self.reference_stack.append(filename) subfile = SubfileCache.Subfile() self.cache[filename] = subfile try: path = self.find_file(filename) with path.open('rb') as file: model = parse.read_ldraw( file, ldraw_directories = self.ldraw_directories, ) except (FileNotFoundError, IOError, PermissionError) as error: subfile.valid = False subfile.problem = str(error) else: if isinstance(model.body[0], linetypes.MetaCommand): subfile.valid = True subfile.description = model.body[0].text else: subfile.valid = False subfile.problem = 'Description not found' if subfile.valid: subfile.vertices = set(parse.model_vertices(model, file_cache = self)) subfile.flatness = {'x', 'y', 'z'} for vertex in subfile.vertices: # Use list(subfile.flatness) for iteration because the # actual set may be modified during the loop for dimension in list(subfile.flatness): if not math.isclose( getattr(vertex, dimension), 0.0, abs_tol = 1e-05, ): subfile.flatness.remove(dimension) if not subfile.flatness: break for subfile_reference in model.subfile_references: # Go through all the subfile references so that we catch any # cyclical references and to get studs self.prepare_file(subfile_reference.subfile_path) subfile.has_studs = any( self.cache[subfile_reference.subfile_path].has_studs or is_logo_stud_name(subfile_reference.subfile_path) for subfile_reference in model.subfile_references ) self.reference_stack.pop() if __name__ == '__main__': cache = SubfileCache(ldraw_directories = ["~/ldraw"])