tests/quadrilaterals.py

Thu, 26 Aug 2021 19:36:44 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Thu, 26 Aug 2021 19:36:44 +0300
changeset 147
bec55b021ae7
parent 106
d93ef722bba6
permissions
-rw-r--r--

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,
    ],
}

mercurial