parse.py

Tue, 25 Aug 2020 23:15:37 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Tue, 25 Aug 2020 23:15:37 +0300
changeset 104
1ad664f783d6
parent 101
745f2c3aec0a
child 127
97de6058109e
permissions
-rw-r--r--

use a bigger threshold in the determinant test

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):
        return self.body[self.header.occurrences[object_type][n]]

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:],
    )

mercurial