Smallest angles

Thu, 21 Dec 2017 10:46:41 +0200

author
Santeri Piippo
date
Thu, 21 Dec 2017 10:46:41 +0200
changeset 7
0ab0d61ccee8
parent 6
6da1e81c5652
child 8
303c51137cb2

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'

mercurial