Initial commit with half-done parsing function

Sun, 10 Dec 2017 15:37:26 +0200

author
Santeri Piippo
date
Sun, 10 Dec 2017 15:37:26 +0200
changeset 0
55b4c97d44c5
child 1
5411a25cfca7

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

mercurial