Thu, 26 Aug 2021 19:36:44 +0300
Merge commit
from math import radians from testsuite import problem_type, report_problem from geometry import * def sign_consistency(container): # Returns whether all elements in container have the same sign return min(container) * max(container) >= 0 @problem_type('concave', severity = 'hold', message = 'concave quadrilateral') def concave_test(model): ''' Checks that all quadrilaterals are convex. ''' for quadrilateral in model.quadrilaterals: # Rotate the polygon into the XY plane. Then z is facing # away from the quadrilateral. geometry = transform_to_xy(quadrilateral.geometry) # Now do a 2D concavity test: # https://math.stackexchange.com/a/1745427 z_scores = [ cross_product(v2 - v1, v3 - v1).z for v1, v2, v3 in pairs(geometry.vertices, count = 3) ] if not sign_consistency(z_scores): yield report_problem('concave', bad_object = quadrilateral) @problem_type('skew-major', severity = 'hold', message = lambda skew_angle: str.format('skew (non-coplanar) quadrilateral (plane angle {})', degree_rep(skew_angle), ), ) @problem_type('skew-minor', severity = 'warning', message = lambda skew_angle: str.format('slightly skew (non-coplanar) quadrilateral (plane angle {})', degree_rep(skew_angle), ), ) def skew_test(model): ''' Checks that all quadrilaterals are coplanar. ''' for quadrilateral in model.quadrilaterals: for triangles in split_quadrilateral(quadrilateral.geometry): plane_1 = triangle_plane_normal(triangles[0]) plane_2 = triangle_plane_normal(triangles[1]) skew_angle = vector_angle(plane_1, plane_2, normalized = True) if skew_angle > radians(3.0): yield report_problem( 'skew-major', bad_object = quadrilateral, skew_angle = skew_angle, ) break elif skew_angle > radians(1.0): yield report_problem( 'skew-minor', bad_object = quadrilateral, skew_angle = skew_angle, ) break @problem_type('self-intersecting', severity = 'hold', message = 'bowtie (self-intersecting) quadrilateral', ) def bowtie_test(model): for quadrilateral in model.quadrilaterals: geometry = transform_to_xy(quadrilateral.geometry) vertices = IndexRing(geometry.vertices) for i in (0, 1): line_1 = LineSegment(vertices[0 + i], vertices[1 + i]) line_2 = LineSegment(vertices[2 + i], vertices[3 + i]) intersection = line_segment_intersection_xy(line_1, line_2) if intersection: yield report_problem( 'self-intersecting', bad_object = quadrilateral, ) break @problem_type('collinear', severity = 'hold', message = lambda min_angle, max_angle: str.format( 'collinear polygon (smallest interior angle {min_angle}, largest {max_angle})', min_angle = degree_rep(min_angle), max_angle = degree_rep(max_angle), ), ) def collinear_test(model): from math import radians, cos, acos # according to the LDraw spec, all interior angles must be # between 0.025 degrees and 179.9 degrees. Note that between 0 and pi, # cos(x) is a downwards curve, so the minimum cosine is for the maximum # angle. min_cosine = cos(radians(179.9)) max_cosine = cos(radians(0.025)) for element in model.body: if hasattr(element, 'geometry') and len(element.geometry.vertices) >= 3: cosines = list(element.geometry.angle_cosines()) if any( not(min_cosine < cosine < max_cosine) for cosine in cosines ): yield report_problem( 'collinear', bad_object = element, min_angle = min(map(acos, cosines)), max_angle = max(map(acos, cosines)), ) manifest = { 'tests': [ skew_test, concave_test, bowtie_test, collinear_test, ], }