Thu, 21 Dec 2017 10:46:41 +0200
Smallest angles
geometry.py | file | annotate | diff | comparison | revisions | |
ldverify.py | file | annotate | diff | comparison | revisions | |
linetypes.py | file | annotate | diff | comparison | revisions |
--- a/geometry.py Wed Dec 20 17:25:09 2017 +0200 +++ b/geometry.py Thu Dec 21 10:46:41 2017 +0200 @@ -1,3 +1,21 @@ +def degree_rep(angle): + from math import degrees + return '∠ %.2f°' % degrees(angle) + +def position_vector(vertex): + assert isinstance(vertex, Vertex) + return Vector(*vertex.coordinates) + +def angle_magnitude_key(angle): + ''' + Returns how great an angle is. + ''' + from math import pi as π + normalized_angle = abs(angle) % (2 * π) + if normalized_angle > π: + normalized_angle = (2 * π) - normalized_angle + return normalized_angle + class Vertex: def __init__(self, x, y, z): if not all(is_real(coordinate) for coordinate in (x, y, z)): @@ -19,18 +37,32 @@ def coordinates(self): return self.x, self.y, self.z def __add__(self, other): - return type(self)(self.x + other.x, self.y + other.y, self.z + other.z) + assert not (type(self) == type(other) == Vertex) + if type(self) == Vector and type(other) == Vector: + result_type = Vector + else: + result_type = Vertex + return result_type(self.x + other.x, self.y + other.y, self.z + other.z) + def __radd__(self, other): + return self + other def __neg__(self): return type(self)(-self.x, -self.y, -self.z) def __sub__(self, other): - return self + (-other) + result = self + (-position_vector(other)) + if isinstance(other, Vertex): + return Vector(*result.coordinates) + else: + return result def __mul__(self, scalar): + assert is_real(scalar) return type(self)(self.x * scalar, self.y * scalar, self.z * scalar) def __rmul__(self, other): return self * other def __truediv__(self, scalar): + assert is_real(scalar) return type(self)(self.x / scalar, self.y / scalar, self.z / scalar) def __floordiv__(self, scalar): + assert is_real(scalar) return type(self)(self.x // scalar, self.y // scalar, self.z // scalar) def __matmul__(self, transformation_matrix): return transform(self, transformation_matrix) @@ -53,6 +85,9 @@ def __repr__(self): return str.format('LineSegment({!r}, {!r})', self.v1, self.v2) @property + def length(self): + return self.v1.distance_to(self.v2) + @property def vertices(self): return self.v1, self.v2 def __len__(self): @@ -70,7 +105,7 @@ def __init__(self, x, y, z): super().__init__(x, y, z) def __repr__(self): - return str.format('Vector({!r}, {!r}. {!r})', self.x, self.y, self.z) + return str.format('Vector({!r}, {!r}, {!r})', self.x, self.y, self.z) def length(self): from math import sqrt return sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2) @@ -148,6 +183,24 @@ return self.vertices[index % len(self.vertices)] def __len__(self): return len(self.vertices) + @property + def perimeter_lines(self): + for v1, v2 in pairs(self.vertices): + yield LineSegment(v1, v2) + @property + def smallest_angle(self): + from math import acos, inf + min_angle = inf + for v1, v2, v3 in pairs(self.vertices, count = 3): + vec1 = (position_vector(v3) - position_vector(v2)).normalized() + vec2 = (position_vector(v1) - position_vector(v2)).normalized() + cosine = dot_product(vec1, vec2) / vec1.length() / vec2.length() + min_angle = min(min_angle, angle_magnitude_key(acos(cosine))) + return min_angle + @property + def hairline_ratio(self): + lengths = [line.length for line in self.perimeter_lines] + return max(lengths) / min(lengths) def is_real(number): return isinstance(number, int) or isinstance(number, float)
--- a/ldverify.py Wed Dec 20 17:25:09 2017 +0200 +++ b/ldverify.py Thu Dec 21 10:46:41 2017 +0200 @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from parse import parse_ldraw_code +from geometry import * def read_ldraw(file, *, libraries): result = list() @@ -7,13 +8,23 @@ result.append(parse_ldraw_code(line)) return result +def hairline_score(smallest_angle): + from math import log10 + return max(0, -log10(smallest_angle)) + 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) - for entry in model: + min_angle_tup = (1e12,) + for line_number, entry in enumerate(model, 1): if hasattr(entry, 'geometry') and len(entry.geometry) >= 3: - print(repr(entry)) - print(entry.geometry.area()) - #print(entry.textual_representation().strip(), end = '\r\n') + if hairline_score(entry.geometry.smallest_angle) >= 2.0: + print(str.format( + 'Hairline {type} at line {line_number}', + type = entry.typename(), + line_number = line_number, + )) + print(entry.textual_representation()) + print('-' * 25) \ No newline at end of file
--- a/linetypes.py Wed Dec 20 17:25:09 2017 +0200 +++ b/linetypes.py Thu Dec 21 10:46:41 2017 +0200 @@ -13,6 +13,8 @@ return 'linetypes.EmptyLine()' def textual_representation(self): return '' + def typename(self): + return 'empty line' class Comment: def __init__(self, text, style = 'old'): @@ -32,6 +34,8 @@ return '0 ' + self.text else: return '0 // ' + self.text + def typename(self): + return 'comment' class SubfileReference: def __init__(self, *, colour, subfile_path, anchor, matrix): @@ -52,6 +56,8 @@ self.subfile_path, ] return ' '.join(ldraw_str(arg) for arg in args) + def typename(self): + return 'subfile reference' class BasePolygon: def __init__(self, *, colour, geometry): @@ -73,14 +79,20 @@ class LineSegment(BasePolygon): def textual_representation(self): return '2 ' + self.base_textual_representation() + def typename(self): + return 'line segment' class Triangle(BasePolygon): def textual_representation(self): return '3 ' + self.base_textual_representation() + def typename(self): + return 'triangle' class Quadrilateral(BasePolygon): def textual_representation(self): return '4 ' + self.base_textual_representation() + def typename(self): + return 'quadrilateral' class Contour(LineSegment): def __init__(self, *, colour, geometry, control_points): @@ -99,3 +111,5 @@ result += ' ' result += ' '.join(strings) return result + def typename(self): + return 'contour line segment'