Sun, 10 Dec 2017 15:37:26 +0200
Initial commit with half-done parsing function
.hgignore | file | annotate | diff | comparison | revisions | |
geometry.py | file | annotate | diff | comparison | revisions | |
ldraw.py | file | annotate | diff | comparison | revisions | |
linetypes.py | file | annotate | diff | comparison | revisions |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Sun Dec 10 15:37:26 2017 +0200 @@ -0,0 +1,3 @@ +syntax:glob +*.dat +__pycache__
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/geometry.py Sun Dec 10 15:37:26 2017 +0200 @@ -0,0 +1,66 @@ +class Vertex: + def __init__(self, x, y, z): + self.x, self.y, self.z = x, y, z + def __repr__(self): + return str.format('Vertex({!r}, {!r}, {!r})', self.x, self.y, self.z) + def distance_to(self, other): + # can't use hypot because of 3 arguments + from math import sqrt + return sqrt( + (self.x - other.x) ** 2 + + (self.y - other.y) ** 2 + + (self.z - other.z) ** 2 + ) + +class LineSegment: + def __init__(self, v1, v2): + self.v1, self.v2 = v1, v2 + def __repr__(self): + return str.format('LineSegment({!r}, {!r})', self.v1, self.v2) + +def is_real(number): + return isinstance(number, int) or isinstance(number, float) + +class TransformationMatrix: + ''' + A 3×3 matrix forming the top-left portion of a full 4×4 transformation + matrix. + ''' + def __init__(self, values): + assert(all(is_real(x) for x in values)) + assert len(values) == 9 + self.values = values + def __repr__(self): + return str.format('TransformationMatrix({!r})', self.values) + def __getitem__(self, index): + return self.values[index] + +def complete_matrix(matrix, anchor): + ''' + Combines a 3×3 matrix and an anchor vertex into a full 4×4 + transformation matrix. + ''' + return [ + matrix[0], matrix[1], matrix[2], anchor.x, + matrix[3], matrix[4], matrix[5], anchor.y, + matrix[6], matrix[7], matrix[8], anchor.z, + 0, 0, 0, 1, + ] + +def transform(vertex, transformation_matrix): + ''' + Transforms a vertex by a 4×4 transformation matrix. + ''' + u = transformation_matrix[0] * vertex.x \ + + transformation_matrix[1] * vertex.y \ + + transformation_matrix[2] * vertex.z \ + + transformation_matrix[3] + v = transformation_matrix[4] * vertex.x \ + + transformation_matrix[5] * vertex.y \ + + transformation_matrix[6] * vertex.z \ + + transformation_matrix[7] + w = transformation_matrix[8] * vertex.x \ + + transformation_matrix[9] * vertex.y \ + + transformation_matrix[10] * vertex.z \ + + transformation_matrix[11] + return Vertex(u, v, w)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ldraw.py Sun Dec 10 15:37:26 2017 +0200 @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +import linetypes +import re +from geometry import * + +class BadLdrawLine(Exception): + pass + +def parse_ldraw_code(line): + line = line.strip() + if not line: + return linetypes.EmptyLine() + if line == '0': + return linetypes.Comment('') + if line.startswith('0 '): + return linetypes.Comment(line[2:].strip()) + if line.startswith('1 '): + return parse_ldraw_subfile_reference(line) + if line.startswith('2 '): + return parse_ldraw_line(line) + ... + +def parse_ldraw_subfile_reference(line): + pattern = r'^1\s+(\d+)' + 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 = { + 'color': 0, + 'anchor': slice(1, 4), + 'matrix': slice(4, 13), + 'subfile_path': 13 + } + try: + color = int(groups[indices['color']]) + 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( + color = color, + anchor = Vertex(*vertex_values), + matrix = TransformationMatrix(matrix_values), + subfile_path = groups[indices['subfile_path']] + ) + +def generic_parse_polygon(line, *, type_code, vertex_count): + pattern = r'^' \ + + str(type_code) \ + + '\s+(\d+)' \ + + r'\s+([^ ]+)' * (vertex_count * 3) \ + + r'\s*$' + 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)) + return { + 'color': int(match.group(1)), + 'vertices': vertices, + } + +def parse_ldraw_line(line): + parse_result = generic_parse_polygon(line, type_code = 2, vertex_count = 2) + line_segment = LineSegment(*parse_result['vertices']) + return linetypes.LineSegment( + color = parse_result['color'], + geometry = line_segment) + +def read_ldraw(file, *, libraries): + result = list() + for line in file: + result.append(parse_ldraw_code(line)) + return result + +if __name__ == '__main__': + from sys import argv + libraries = [{'path': '/home/teemu/ldraw', 'role': 'official'}] + with open(argv[1], 'r') as file: + model = read_ldraw(file, libraries = libraries) + from pprint import pprint + pprint(model)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/linetypes.py Sun Dec 10 15:37:26 2017 +0200 @@ -0,0 +1,46 @@ +class EmptyLine: + def __repr__(self): + return 'linetypes.EmptyLine()' + +class Comment: + def __init__(self, text, style = 'old'): + if style == 'old' and text.startswith('//'): + self.text = text[3:].strip() + self.style = 'new' + else: + self.text = text + self.style = style + def __repr__(self): + return str.format('linetypes.Comment({text!r}, {style!r})', + text = self.text, + style = self.style, + ) + +class SubfileReference: + def __init__(self, *, color, subfile_path, anchor, matrix): + self.color, self.subfile_path, = color, subfile_path + self.anchor, self.matrix = anchor, matrix + def __repr__(self): + return str.format('linetypes.SubfileReference(' \ + 'color = {color!r}, ' \ + 'subfile_path = {subfile_path!r}, ' \ + 'anchor = {anchor!r}, ' \ + 'matrix = {matrix!r})', **self.__dict__) + +class LineSegment: + def __init__(self, *, color, geometry): + self.color, self.geometry = color, geometry + def __repr__(self): + return str.format('linetypes.{typename}(' \ + 'color = {color!r}, ' \ + 'geometry = {geometry!r})', + typename = type(self).__name__, + color = self.color, + geometry = self.geometry, + ) + +class Triangle(LineSegment): + pass + +class Quadrilateral(LineSegment): + pass