Fri, 18 Sep 2020 21:41:38 +0300
added moved to with extension unit tests
import linetypes import re from geometry import * from colours import Colour import header class BadLdrawLine(Exception): pass class Model: def __init__( self, header, body, *, ldraw_directories, \ header_size = 0, line_ending_errors = None ): self.header = header self.body = body self.header_size = header_size self.ldraw_directories = ldraw_directories self.line_ending_errors = line_ending_errors def filter_by_type(self, type): yield from [ element for element in self.body if isinstance(element, type) ] @property def subfile_references(self): yield from self.filter_by_type(linetypes.SubfileReference) @property def line_segments(self): yield from self.filter_by_type(linetypes.LineSegment) @property def triangles(self): yield from self.filter_by_type(linetypes.Triangle) @property def quadrilaterals(self): yield from self.filter_by_type(linetypes.Quadrilateral) @property def has_header(self): return self.header and not isinstance(self.header, header.BadHeader) def find_first_header_object(self, object_type): return self.find_header_object(object_type, 0) def find_header_object(self, object_type, n): try: return self.body[self.header.occurrences[object_type][n]] except IndexError: raise KeyError(str.format( '{type} not found in header', type = object_type )) def model_vertices( model, transformation_matrix = None, file_cache = None, ): if transformation_matrix is None: transformation_matrix = complete_matrix(Matrix3x3(), Vertex(0, 0, 0)) if file_cache is None: import filecache file_cache = filecache.SubfileCache(model.ldraw_directories) for element in model.body: if isinstance(element, linetypes.BasePolygon): for point in element.geometry.vertices: yield point @ transformation_matrix if isinstance(element, linetypes.ConditionalLine): for point in element.control_points: yield point @ transformation_matrix if isinstance(element, linetypes.SubfileReference): subfile = file_cache.prepare_file(element.subfile_path) for point in subfile.vertices: matrix_4x4 = complete_matrix(element.matrix, element.anchor) point @= matrix_4x4 yield point @ transformation_matrix def read_ldraw(file, *, ldraw_directories, name = ''): line_ending_errors = { 'count': 0, 'first-at': None, } model_body = [] for i, line in enumerate(file.readlines()): # check line endings if not line.endswith(b'\r\n'): if line_ending_errors['first-at'] is None: line_ending_errors['first-at'] = i line_ending_errors['count'] += 1 model_body.append(parse_ldraw_code(line)) if line_ending_errors['count'] == 0: line_ending_errors = None headerparser = header.HeaderParser() try: header_parse_result = headerparser.parse(model_body) header_object = header_parse_result['header'] end = header_parse_result['end-index'] except header.HeaderError as error: header_object = header.BadHeader(error.index, error.reason) end = 0 model = Model( header = header_object, body = model_body, header_size = end, ldraw_directories = ldraw_directories, line_ending_errors = line_ending_errors, ) model.name = name return model def parse_ldraw_code(line): try: if isinstance(line, bytes): line = line.decode() line = line.strip() if not line: return linetypes.EmptyLine() elif line == '0': return linetypes.MetaCommand('') elif line.startswith('0 '): return parse_ldraw_meta_line(line) elif line.startswith('1 '): return parse_ldraw_subfile_reference(line) elif line.startswith('2 '): return parse_ldraw_line(line) elif line.startswith('3 '): return parse_ldraw_triangle(line) elif line.startswith('4 '): return parse_ldraw_quadrilateral(line) elif line.startswith('5 '): return parse_ldraw_conditional_line(line) else: raise BadLdrawLine('unknown line type') except BadLdrawLine as error: return linetypes.Error(line, str(error)) def parse_ldraw_meta_line(line): if line.startswith('0 //'): return linetypes.Comment(line[4:]) else: return linetypes.MetaCommand(line[2:]) def parse_ldraw_subfile_reference(line): pattern = r'^1\s+([^ ]+)' + r'\s+([^ ]+)' * (3 + 9 + 1) + r'\s*$' match = re.search(pattern, line) if not match: raise BadLdrawLine('unable to parse') groups = list(match.groups()) indices = { 'colour_index': 0, 'anchor': slice(1, 4), 'matrix': slice(4, 13), 'subfile_path': 13 } try: colour = Colour(groups[indices['colour_index']]) vertex_values = [float(x) for x in groups[indices['anchor']]] matrix_values = [float(x) for x in groups[indices['matrix']]] except ValueError: raise BadLdrawLine('bad numeric values') return linetypes.SubfileReference( colour = colour, anchor = Vertex(*vertex_values), matrix = Matrix3x3(matrix_values), subfile_path = groups[indices['subfile_path']] ) def generic_parse_polygon(line, *, type_code, vertex_count): pattern = r'^' # matches the start of line pattern += str(type_code) # matches the type code pattern += '\s+([^ ]+)' # matches the colour pattern += r'\s+([^ ]+)' * (vertex_count * 3) # matches the vertices pattern += r'\s*$' # matches any trailing space match = re.search(pattern, line) if not match: raise BadLdrawLine(str.format('cannot parse type-{} line', type_code)) vertices = [] for vertex_index in range(vertex_count): slice_begin = 1 + vertex_index * 3 slice_end = 1 + (vertex_index + 1) * 3 coordinates = match.groups()[slice_begin:slice_end] assert(len(coordinates) == 3) try: coordinates = [float(x) for x in coordinates] except ValueError: raise BadLdrawLine('bad numeric values') vertices.append(Vertex(*coordinates)) try: colour = int(match.group(1), 0) except ValueError: raise BadLdrawLine('invalid syntax for colour: ' + repr(match.group(1))) return { 'colour': Colour(colour), 'vertices': vertices, } def parse_ldraw_line(line): parse_result = generic_parse_polygon(line, type_code = 2, vertex_count = 2) return linetypes.LineSegment( colour = parse_result['colour'], geometry = LineSegment(*parse_result['vertices']), ) def parse_ldraw_triangle(line): parse_result = generic_parse_polygon(line, type_code = 3, vertex_count = 3) return linetypes.Triangle( colour = parse_result['colour'], geometry = Polygon(parse_result['vertices']), ) def parse_ldraw_quadrilateral(line): parse_result = generic_parse_polygon(line, type_code = 4, vertex_count = 4) return linetypes.Quadrilateral( colour = parse_result['colour'], geometry = Polygon(parse_result['vertices']), ) def parse_ldraw_conditional_line(line): parse_result = generic_parse_polygon(line, type_code = 5, vertex_count = 4) return linetypes.ConditionalLine( colour = parse_result['colour'], geometry = LineSegment(*parse_result['vertices'][0:2]), control_points = parse_result['vertices'][2:], )