Mon, 24 Jun 2019 00:54:24 +0300
fixed prefixed punctuations winding up in the effective categories of subparts
from testsuite import problem_type, report_problem import testsuite from geometry import * from os.path import dirname from pathlib import Path from configparser import ConfigParser import math ini_path = Path(dirname(__file__)) / 'library-standards.ini' library_standards = ConfigParser() with ini_path.open() as file: library_standards.read_file(file) @problem_type('zero-determinant', severity = 'hold', message = 'matrix row or column all zero' ) def determinant_test(model): ''' Checks all subfile references for matrices with rows or columns all zero. ''' yield from ( report_problem('zero-determinant', bad_object = subfile_reference) for subfile_reference in model.subfile_references if abs(subfile_reference.matrix.determinant() - 0) < 1e-15 ) def scaling_description(scaling, axes = 'xyz'): ''' Returns a pretty description of a scaling vector. The axes parameter controls what axes are printed and can be used to filter away uninteresting values. ''' if isinstance(scaling, str): return scaling else: return ', '.join( str.format('{} = {}', letter, getattr(scaling, letter)) for letter in axes ) def check_scaling(scaling, axes): ''' Returns whether all given axes on the given scaling vector are 1. ''' return all( abs(getattr(scaling, axis) - 1) < 1e-5 for axis in axes ) # Restriction to checking function mapping. restriction_tests = { 'no scaling': lambda scaling: check_scaling(scaling, 'xyz'), 'y-scaling only': lambda scaling: check_scaling(scaling, 'xz'), 'stud3-like scaling': lambda scaling: all([ check_scaling(scaling, 'xz'), # check if y-scaling is 1 or -1 abs(abs(scaling.y) - 1) < 1e-5, ]), } @problem_type('illegal-scaling', severity = 'hold', message = lambda primitive, scaling, axes: str.format('scaling of unscalable primitive {} ({})', primitive, scaling_description(scaling, axes), ), ) def scaling_legality_test(model): ''' Checks the part against primitive references with bad scaling. Some primitives (e.g. pegs) are not allowed to be scaled in the X or Z directions. Some (e.g. most studs) are not allowed to be scaled in the Y direction either. ''' from fnmatch import fnmatch scaling_restrictions = library_standards['scaling restrictions'] for subfile_reference in model.subfile_references: primitive = subfile_reference.subfile_path.lower() scaling = subfile_reference.matrix.scaling_vector() # Find all restrictions that apply to this subfile reference. restrictions = { restriction for pattern, restriction in scaling_restrictions.items() if fnmatch(primitive, pattern) } # Check restrictions against the subfile. If any restrictions were # found then the scaling vector must pass at least one of them. if restrictions and not any( restriction_tests[restriction](scaling) for restriction in restrictions ): interesting_axes = ''.join( axis for axis in 'xyz' if abs(getattr(scaling, axis) - 1) > 1e-5 ) yield report_problem('illegal-scaling', bad_object = subfile_reference, primitive = primitive, axes = interesting_axes, scaling = scaling, ) def subfile_references_with_invertnext(model): import linetypes has_invertnext = False for element in model.body: if isinstance(element, linetypes.MetaCommand) \ and element.text == 'BFC INVERTNEXT': has_invertnext = True else: if isinstance(element, linetypes.SubfileReference): yield element, has_invertnext has_invertnext = False @problem_type('cyclical-reference', severity = 'hold', message = lambda chain: str.format('cyclical subfile dependency: {chain}', **locals(), ), ) @problem_type('bad-subfile', severity = 'hold', message = lambda path, problem_text: str.format('cannot process subfile "{path}": {problem_text}', **locals(), ), ) @problem_type('moved-file-used', severity = 'hold', message = lambda moved_file, new_file: str.format('subfile "{moved_file}" has been moved to "{new_file}"', **locals(), ), ) @problem_type('unnecessary-scaling', severity = 'warning', message = lambda scaled_flat_dimensions, scaling_vector: str.format( 'subfile unnecessarily scaled in the {dims} ({scaling})', dims = dimensions_description(scaled_flat_dimensions), scaling = scaling_description(scaling_vector), ), ) @problem_type('unnecessary-invertnext', severity = 'warning', message = 'flat subfile unnecessarily inverted using BFC INVERTNEXT' ) def dependent_subfile_tests(model): ''' Tests subfile references for such qualities that are dependent on the actual contents of the subfiles. Checks whether moved-to files are used. Checks whether flat subfiles are scaled in the flat direction. ''' import filecache cache = filecache.SubfileCache(model.ldraw_directories) failed_subfiles = set() for subfile_reference, has_invertnext in subfile_references_with_invertnext(model): path = subfile_reference.subfile_path.lower() if path in failed_subfiles: # Already proven to be a bad apple, don't complain twice pass else: try: subfile = cache.prepare_file(path) except filecache.CyclicalReferenceError as e: failed_subfiles.add(path) yield report_problem( 'cyclical-reference', bad_object = subfile_reference, chain = str(e), ) if not subfile.valid: yield report_problem( 'bad-subfile', bad_object = subfile_reference, path = path, problem_text = subfile.problem, ) failed_subfiles.add(path) else: # Test for use of moved-to files import re match = re.search(r'^\~Moved(?: to (\w+))?$', subfile.description) if match: yield report_problem( 'moved-file-used', bad_object = subfile_reference, moved_file = path, new_file = match.group(1), ) # Test for scaling in flat direction scaling_vector = subfile_reference.matrix.scaling_vector() scaled_dimensions = { dimension for dimension in subfile.flatness if not math.isclose( getattr(scaling_vector, dimension), 1, abs_tol = 1.0e-05 ) } scaled_flat_dimensions = subfile.flatness & scaled_dimensions if scaled_flat_dimensions: yield report_problem( 'unnecessary-scaling', bad_object = subfile_reference, scaled_flat_dimensions = scaled_flat_dimensions, scaling_vector = scaling_vector, ) # Test whether a flat subfile is inverted using invertnext if has_invertnext and subfile.flatness: yield report_problem( 'unnecessary-invertnext', bad_object = subfile_reference, ) def dimensions_description(dimensions): if isinstance(dimensions, str): return dimensions else: sorted_dims = sorted(dimensions) if len(sorted_dims) == 1: return sorted_dims[0] + ' dimension' else: return str.format('{} and {} dimensions', ', '.join(sorted_dims[:-1]), sorted_dims[-1], ) @problem_type('bad-category', severity = 'hold', message = lambda category: str.format( '"{category}" is not an official category', category = category, ) ) @problem_type('bad-category-in-description', severity = 'hold', message = lambda category: str.format( 'the category "{category}" must be set using !CATEGORY ' 'and not by description', category = category, ) ) def category_test(model): if model.header.valid: categories = library_standards['categories'] illegal_categories_in_description = [ category_name.lower() for category_name in categories.keys() if ' ' in category_name ] has_bad_category = False if model.header.effective_category not in categories.keys(): try: bad_object = model.find_first_header_object('category') except KeyError: # category was not specified using !CATEGORY, blame # the description instead bad_object = model.body[0] has_bad_category = True yield report_problem( 'bad-category', bad_object = bad_object, category = model.header.effective_category, ) # Check if the description sets a multi-word category if not has_bad_category and model.header.category is None: for category_name in illegal_categories_in_description: if model.header.description.lower().startswith(category_name): yield report_problem( 'bad-category-in-description', bad_object = model.body[0], category = category_name.title(), ) break @problem_type('mirrored-studs', severity = 'warning', message = lambda primitive: str.format( '"{primitive}" should not be mirrored', primitive = primitive, ) ) def mirrored_studs_test(model): for subfile_reference in model.subfile_references: # Test whether any stud subfile is mirrored. # A subfile is mirrored if its determinant is negative. if subfile_reference.subfile_path.startswith('stu') \ and subfile_reference.subfile_path != 'stud4.dat' \ and subfile_reference.matrix.determinant() < 0: yield report_problem( 'mirrored-studs', bad_object = subfile_reference, primitive = subfile_reference.subfile_path, ) manifest = { 'tests': [ determinant_test, scaling_legality_test, dependent_subfile_tests, category_test, mirrored_studs_test, ], }