From 8664f0a2095f2d0b7ef165929a633bd7463c99ad Mon Sep 17 00:00:00 2001 From: theoldben Date: Mon, 12 Aug 2019 16:51:52 +0200 Subject: [PATCH] Create topology_polechecks Adds two checks for 3-valence and 5-valence poles to the MeshLint set. --- topology_polechecks | 912 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 912 insertions(+) create mode 100644 topology_polechecks diff --git a/topology_polechecks b/topology_polechecks new file mode 100644 index 0000000..cf2708f --- /dev/null +++ b/topology_polechecks @@ -0,0 +1,912 @@ +# TODO: +# - Exempt mirror-plane verts. You should not get penalized for them. +# - Check for intersected faces?? +# - Would probably be O(n^m) or something. +# - Would need to check the post-modified mesh (e.g., Armature-deformed) +# - Coplanar check, especially good for Ngons. +# - Check Normal consistency? I've had several people request this, though I +# still feel like the Ctrl+n tool has problems solving it, so I am +# unconfident that I will be able to do as good or better. It is true, +# though, that you can simply allow the user to enable the check, and if it +# is acting wonky they can disable it. +# - Consider adding to the 'n' Properties Panel instead of Object Data. Or, +# perhaps, a user preference. +# - Maybe add a "Skip to Next" option. So far at least 1 user has reported +# this. Personally, I think you should hit Tab and deselect the one you +# want to skip, but I haven't thought it through too far. + +bl_info = { + "name": "MeshLint: Like Spell-checking for your Meshes", + "author": "rking (Port to 2.80 Sav Martin)", + "version": (1, 0), + "blender": (2, 80, 0), + "location": "Object Data properties > MeshLint", + "description": "Check objects for: Tris / Ngons / Nonmanifoldness / etc", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/MeshLint", + "tracker_url": "https://github.com/ryanjosephking/meshlint/issues", + "category": "Mesh"} + +# For the ./mkblenderwiki script +mkblenderwiki_info = { + "license": "GPL", + "py_download": "https://raw.github.com/ryanjosephking/meshlint/master/meshlint.py", + "git_download": "https://github.com/ryanjosephking/meshlint.git", + "input_img_prefix": "meshlint/raw/master/img/", + "wiki_img_prefix": "Scripts-Modeling-MeshLint-"} + +# Look for the "seeing error text", below. Something is super-fishy, but this +# is the workaround. +try: + import bpy + import bmesh + import time + import re + from mathutils import Vector + + SUBPANEL_LABEL = 'MeshLint' + COMPLAINT_TIMEOUT = 3 # seconds + ELEM_TYPES = [ 'verts', 'edges', 'faces' ] + + N_A_STR = '(N/A - disabled)' + TBD_STR = '...' + + + def is_edit_mode(): + return 'EDIT_MESH' == bpy.context.mode + + def ensure_edit_mode(): + if not is_edit_mode(): + bpy.ops.object.editmode_toggle() + + + def ensure_not_edit_mode(): + if is_edit_mode(): + bpy.ops.object.editmode_toggle() + + + def has_active_mesh(context): + obj = context.active_object + return obj and 'MESH' == obj.type + + + class MeshLintAnalyzer: + CHECKS = [] + + def __init__(self): + self.obj = bpy.context.active_object + ensure_edit_mode() + self.b = bmesh.from_edit_mesh(self.obj.data) + self.num_problems_found = None + + def find_problems(self): + analysis = [] + self.num_problems_found = 0 + for lint in MeshLintAnalyzer.CHECKS: + sym = lint['symbol'] + should_check = getattr(bpy.context.scene, lint['check_prop']) + if not should_check: + lint['count'] = N_A_STR + continue + lint['count'] = 0 + check_method_name = 'check_' + sym + check_method = getattr(type(self), check_method_name) + bad = check_method(self) + report = { 'lint': lint } + for elemtype in ELEM_TYPES: + indices = bad.get(elemtype, []) + report[elemtype] = indices + lint['count'] += len(indices) + self.num_problems_found += len(indices) + analysis.append(report) + return analysis + + def found_zero_problems(self): + return 0 == self.num_problems_found + + @classmethod + def none_analysis(cls): + analysis = [] + for lint in cls.CHECKS: + row = { elemtype: [] for elemtype in ELEM_TYPES } + row['lint'] = lint + analysis.append(row) + return analysis + + CHECKS.append({ + 'symbol': 'tris', + 'label': 'Tris', + 'definition': 'A face with 3 edges. Often bad for modeling because it stops edge loops and does not deform well around bent areas. A mesh might look good until you animate, so beware!', + 'default': True + }) + def check_tris(self): + bad = { 'faces': [] } + for f in self.b.faces: + if 3 == len(f.verts): + bad['faces'].append(f.index) + return bad + + CHECKS.append({ + 'symbol': 'ngons', + 'label': 'Ngons', + 'definition': 'A face with >4 edges. Is generally bad in exactly the same ways as Tris', + 'default': True + }) + def check_ngons(self): + bad = { 'faces': [] } + for f in self.b.faces: + if 4 < len(f.verts): + bad['faces'].append(f.index) + return bad + + CHECKS.append({ + 'symbol': 'nonmanifold', + 'label': 'Nonmanifold Elements', + 'definition': 'Simply, shapes that won\'t hold water. More precisely, nonmanifold edges are those that do not have exactly 2 faces attached to them (either more or less). Nonmanifold verts are more complicated -- you can see their definition in BM_vert_is_manifold() in bmesh_queries.c', + 'default': True + }) + def check_nonmanifold(self): + bad = {} + for elemtype in 'verts', 'edges': + bad[elemtype] = [] + for elem in getattr(self.b, elemtype): + if not elem.is_manifold: + bad[elemtype].append(elem.index) + # TODO: Exempt mirror-plane verts. + # Plus: ...anybody wanna tackle Mirrors with an Object Offset? + return bad + + CHECKS.append({ + 'symbol': 'interior_faces', + 'label': 'Interior Faces', + 'definition': 'This confuses people. It is very specific: A face whose edges ALL have >2 faces attached. The simplest way to see this is to Ctrl+r a Default Cube and hit \'f\'', + 'default': True + }) + def check_interior_faces(self): # translated from editmesh_select.c + bad = { 'faces': [] } + for f in self.b.faces: + if not any(3 > len(e.link_faces) for e in f.edges): + bad['faces'].append(f.index) + return bad + + CHECKS.append({ + 'symbol': 'three_poles', + 'label': '3-edge Poles', + 'definition': 'A vertex with 3 edges connected to it. Also known as an N-Pole', + 'default': False + }) + def check_three_poles(self): + bad = { 'verts': [] } + for v in self.b.verts: + if 3 == len(v.link_edges): + bad['verts'].append(v.index) + return bad + + CHECKS.append({ + 'symbol': 'five_poles', + 'label': '5-edge Poles', + 'definition': 'A vertex with 5 edges connected to it. Also known as an E-Pole', + 'default': False + }) + + def check_five_poles(self): + bad = {'verts': [] } + for v in self.b.verts: + if 5 == len(v.link_edges): + bad['verts'].append(v.index) + return bad + + CHECKS.append({ + 'symbol': 'sixplus_poles', + 'label': '6+-edge Poles', + 'definition': 'A vertex with 6 or more edges connected to it. Generally this is not something you want, but since some kinds of extrusions will legitimately cause such a pole (imagine extruding each face of a Cube outward, the inner corners are rightful 6+-poles). Still, if you don\'t know for sure that you want them, it is good to enable this', + 'default': True + }) + + def check_sixplus_poles(self): + bad = { 'verts': [] } + for v in self.b.verts: + if 5 < len(v.link_edges): + bad['verts'].append(v.index) + return bad + # [Your great new idea here] -> Tell me about it: rking@panoptic.com + + # ...plus the 'Default Name' check. + + def enable_anything_select_mode(self): + self.b.select_mode = {'VERT', 'EDGE', 'FACE'} + + def select_indices(self, elemtype, indices): + for i in indices: + if 'verts' == elemtype: + self.select_vert(i) + elif 'edges' == elemtype: + self.select_edge(i) + elif 'faces' == elemtype: + self.select_face(i) + else: + print("MeshLint says: Huh?? → elemtype of %s." % elemtype) + + def select_vert(self, index): + ob = bpy.context.edit_object + me = ob.data + bm = bmesh.from_edit_mesh(me) + bm.verts.ensure_lookup_table() #sav + self.b.verts[index].select = True + + def select_edge(self, index): + ob = bpy.context.edit_object + me = ob.data + bm = bmesh.from_edit_mesh(me) + bm.edges.ensure_lookup_table() #sav + edge = self.b.edges[index] + edge.select = True + for each in edge.verts: + self.select_vert(each.index) + + def select_face(self, index): + ob = bpy.context.edit_object + me = ob.data + bm = bmesh.from_edit_mesh(me) + bm.faces.ensure_lookup_table() #sav + face = self.b.faces[index] + face.select = True + for each in face.edges: + self.select_edge(each.index) + + def topology_counts(self): + data = self.obj.data + return { + 'data': self.obj.data, + 'faces': len(self.b.faces), + 'edges': len(self.b.edges), + 'verts': len(self.b.verts) } + + for lint in CHECKS: + sym = lint['symbol'] + lint['count'] = TBD_STR + prop = 'meshlint_check_' + sym + lint['check_prop'] = prop + 'meshlint_check_' + sym + setattr( + bpy.types.Scene, + prop, + bpy.props.BoolProperty( + default=lint['default'], + description=lint['definition'])) + + + @bpy.app.handlers.persistent + def global_repeated_check(dummy): + MeshLintContinuousChecker.check() + + + class MeshLintContinuousChecker(): + current_message = '' + time_complained = 0 + previous_topology_counts = None + previous_analysis = None + + @classmethod + def check(cls): + if not is_edit_mode(): + return + analyzer = MeshLintAnalyzer() + now_counts = analyzer.topology_counts() + previous_topology_counts = \ + cls.previous_topology_counts + if not None is previous_topology_counts: + previous_data_name = previous_topology_counts['data'].name + else: + previous_data_name = None + now_name = now_counts['data'].name + if None is previous_topology_counts \ + or now_counts != previous_topology_counts: + if not previous_data_name == now_name: + before = MeshLintAnalyzer.none_analysis() + analysis = analyzer.find_problems() + diff_msg = cls.diff_analyses( + cls.previous_analysis, analysis) + if not None is diff_msg: + cls.announce(diff_msg) + cls.time_complained = time.time() + cls.previous_topology_counts = now_counts + cls.previous_analysis = analysis + if not None is cls.time_complained \ + and COMPLAINT_TIMEOUT < time.time() - cls.time_complained: + cls.announce(None) + cls.time_complained = None + + @classmethod + def diff_analyses(cls, before, after): + if None is before: + before = MeshLintAnalyzer.none_analysis() + report_strings = [] + dict_before = cls.make_labels_dict(before) + dict_now = cls.make_labels_dict(after) + for check in MeshLintAnalyzer.CHECKS: + check_name = check['label'] + if not check_name in dict_now.keys(): + continue + report = dict_now[check_name] + report_before = dict_before.get(check_name, {}) + check_elem_strings = [] + for elemtype, elem_list in report.items(): + elem_list_before = report_before.get(elemtype, []) + if len(elem_list) > len(elem_list_before): + count_diff = len(elem_list) - len(elem_list_before) + elem_string = depluralize( + count=count_diff, string=elemtype) + check_elem_strings.append( + str(count_diff) + ' ' + elem_string) + if len(check_elem_strings): + report_strings.append( + check_name + ': ' + ', '.join(check_elem_strings)) + if len(report_strings): + return 'Found ' + ', '.join(report_strings) + return None + + @classmethod + def make_labels_dict(cls, analysis): + if None is analysis: + return {} + labels_dict = {} + for check in analysis: + label = check['lint']['label'] + new_val = check.copy() + del new_val['lint'] + labels_dict[label] = new_val + return labels_dict + + @classmethod + def announce(cls, message): + for area in bpy.context.screen.areas: + if 'INFO' != area.type: + continue + if None is message: + area.header_text_set() + else: + area.header_text_set('MeshLint: ' + message) + + + class MeshLintVitalizer(bpy.types.Operator): + 'Toggles the real-time execution of the checks (Edit Mode only)' + bl_idname = 'meshlint.live_toggle' + bl_label = 'MeshLint Live Toggle' + + is_live = False + + @classmethod + def poll(cls, context): + return has_active_mesh(context) and is_edit_mode() + + def execute(self, context): + if MeshLintVitalizer.is_live: + bpy.app.handlers.depsgraph_update_post.remove(global_repeated_check) #sav + MeshLintVitalizer.is_live = False + else: + bpy.app.handlers.depsgraph_update_post.append(global_repeated_check) #sav + MeshLintVitalizer.is_live = True + return {'FINISHED'} + + + def activate(obj): + # bpy.context.scene.objects.active = obj #sav + bpy.context.view_layer.objects.active = obj + + + class MeshLintObjectLooper: + def examine_active_object(self): + analyzer = MeshLintAnalyzer() + analyzer.enable_anything_select_mode() + self.select_none() + analysis = analyzer.find_problems() + for lint in analysis: + for elemtype in ELEM_TYPES: + indices = lint[elemtype] + analyzer.select_indices(elemtype, indices) + return analyzer.found_zero_problems() + + def examine_all_selected_meshes(self): + self.original_active = bpy.context.active_object + self.troubled_meshes = [] + examinees = [self.original_active] + bpy.context.selected_objects + for obj in examinees: + if 'MESH' != obj.type: + continue + activate(obj) + good = self.examine_active_object() + ensure_not_edit_mode() + if not good: + self.troubled_meshes.append(obj) + priorities = [self.original_active] + self.troubled_meshes + for obj in priorities: + if obj.select_get: + activate(obj) + break + self.handle_troubled_meshes() + bpy.context.area.tag_redraw() + + def select_none(self): + bpy.ops.mesh.select_all(action='DESELECT') + + class MeshLintSelector(MeshLintObjectLooper, bpy.types.Operator): + 'Uncheck boxes below to prevent those checks from running' + bl_idname = 'meshlint.select' + bl_label = 'MeshLint Select' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return has_active_mesh(context) + + def execute(self, context): + original_mode = bpy.context.mode + if is_edit_mode(): + self.examine_active_object() + else: + self.examine_all_selected_meshes() + if len(self.troubled_meshes): + ensure_edit_mode() + elif 'EDIT_MESH' != original_mode: + ensure_not_edit_mode() + return {'FINISHED'} + + def handle_troubled_meshes(self): + pass + + class MeshLintObjectDeselector(MeshLintObjectLooper, bpy.types.Operator): + 'Uncheck boxes below to prevent those checks from running (Object Mode only)' + bl_idname = 'meshlint.objects_deselect' + bl_label = 'MeshLint Objects Deselect' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + selected_meshses = [ + o for o in bpy.context.selected_objects if o.type == 'MESH'] + return 1 < len(selected_meshses) and not is_edit_mode() + + def execute(self, context): + self.examine_all_selected_meshes() + return {'FINISHED'} + + def handle_troubled_meshes(self): + for obj in bpy.context.selected_objects: + if not obj in self.troubled_meshes: + obj.select = False + + class MESH_PT_MeshLintControl(bpy.types.Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'data' + bl_label = SUBPANEL_LABEL + + @classmethod + def poll(cls, context): + return has_active_mesh(context) + + def draw(self, context): + layout = self.layout + self.add_main_buttons(layout) + self.add_criticism(layout, context) + self.add_toggle_buttons(layout, context) + + def add_main_buttons(self, layout): + split = layout.split() + left = split.column() + left.operator( + 'meshlint.select', text='Select Lint', icon='EDITMODE_HLT') + + right = split.column() + if MeshLintVitalizer.is_live: + live_label = 'Pause Checking...' + play_pause = 'PAUSE' + else: + live_label = 'Continuous Check!' + play_pause = 'PLAY' + right.operator( + 'meshlint.live_toggle', text=live_label, icon=play_pause) + + layout.split().operator( + 'meshlint.objects_deselect', + text='Deselect all Lint-free Objects', + icon='UV_ISLANDSEL') + + def add_criticism(self, layout, context): + col = layout.column() + active = context.active_object + if not has_active_mesh(context): + return + total_problems = 0 + for lint in MeshLintAnalyzer.CHECKS: + count = lint['count'] + if count in (TBD_STR, N_A_STR): + label = str(count) + ' ' + lint['label'] + reward = 'SOLO_OFF' + elif 0 == count: + label = 'No %s!' % lint['label'] + reward = 'SOLO_ON' + else: + total_problems += count + label = str(count) + 'x ' + lint['label'] + label = depluralize(count=count, string=label) + reward = 'ERROR' + col.row().label(text=label, icon=reward) + name_crits = MESH_PT_MeshLintControl.build_object_criticisms( + bpy.context.selected_objects, total_problems) + for crit in name_crits: + col.row().label(text=crit) + + def add_toggle_buttons(self, layout, context): + col = layout.column() + col.row().label(text='Toggle:') + for lint in MeshLintAnalyzer.CHECKS: + prop_name = lint['check_prop'] + is_enabled = getattr(context.scene, prop_name) + label = 'Check ' + lint['label'] + col.row().prop(context.scene, prop_name, text=label) + + @classmethod + def build_object_criticisms(cls, objects, total_problems): + already_complained = total_problems > 0 + criticisms = [] + def add_crit(crit): + if already_complained: + conjunction = 'and also' + else: + conjunction = 'but' + criticisms.append('...%s "%s" %s.' % ( + conjunction, obj.name, crit)) + for obj in objects: + if MESH_PT_MeshLintControl.has_unapplied_scale(obj.scale): + add_crit('has an unapplied scale') + already_complained = True + if MESH_PT_MeshLintControl.is_bad_name(obj.name): + add_crit('is not a great name') + already_complained = True + return criticisms + + @classmethod + def has_unapplied_scale(cls, scale): + return 3 != len([c for c in scale if c == 1.0]) + + @classmethod + def is_bad_name(cls, name): + default_names = [ + 'BezierCircle', + 'BezierCurve', + 'Circle', + 'Cone', + 'Cube', + 'CurvePath', + 'Cylinder', + 'Grid', + 'Icosphere', + 'Mball', + 'Monkey', + 'NurbsCircle', + 'NurbsCurve', + 'NurbsPath', + 'Plane', + 'Sphere', + 'Surface', + 'SurfCircle', + 'SurfCurve', + 'SurfCylinder', + 'SurfPatch', + 'SurfSphere', + 'SurfTorus', + 'Text', + 'Torus', + ] + pat = '(%s)\.?\d*$' % '|'.join(default_names) + return not None is re.match(pat, name) + + + def depluralize(**args): + if 1 == args['count']: + return args['string'].rstrip('s') + else: + return args['string'] + + + # Hrm. Why does it work for some Blender's but not others? + try: + import unittest + import warnings + + class TestControl(unittest.TestCase): + def test_scale_application(self): + for bad in [ [0,0,0], [1,2,3], [1,1,1.1] ]: + self.assertEqual( + True, MESH_PT_MeshLintControl.has_unapplied_scale(bad), + "Unapplied scale: %s" % bad) + self.assertEqual( + False, MESH_PT_MeshLintControl.has_unapplied_scale([1,1,1]), + "Applied scale (1,1,1)") + + def test_bad_names(self): + for bad in [ 'Cube', 'Cube.001', 'Sphere.123' ]: + self.assertEqual( + True, MESH_PT_MeshLintControl.is_bad_name(bad), + "Bad name: %s" % bad) + for ok in [ 'Whatever', 'NumbersOkToo.001' ]: + self.assertEqual( + False, MESH_PT_MeshLintControl.is_bad_name(ok), + "OK name: %s" % ok) + + + class TestUtilities(unittest.TestCase): + def test_depluralize(self): + self.assertEqual( + 'foo', + depluralize(count=1, string='foos')) + self.assertEqual( + 'foos', + depluralize(count=2, string='foos')) + + + class TestAnalysis(unittest.TestCase): + def test_make_labels_dict(self): + self.assertEqual( + { + 'Label One': { + 'edges': [1,2], 'verts': [], 'faces': [] }, + 'Label Two': { + 'edges': [], 'verts': [5], 'faces': [3] } + }, + MeshLintContinuousChecker.make_labels_dict( + [ + { 'lint': { 'label': 'Label One' }, + 'edges': [1,2], 'verts': [], 'faces': [] }, + { 'lint': { 'label': 'Label Two' }, + 'edges': [], 'verts': [5], 'faces': [3] } + ]), + 'Conversion of incoming analysis into label-keyed dict') + self.assertEqual( + {}, + MeshLintContinuousChecker.make_labels_dict(None), + 'Handles "None" OK.') + + def test_comparison(self): + self.assertEqual( + None, + MeshLintContinuousChecker.diff_analyses( + MeshLintAnalyzer.none_analysis(), + MeshLintAnalyzer.none_analysis()), + 'Two none_analysis()s') + self.assertEqual( + 'Found Tris: 4 verts', + MeshLintContinuousChecker.diff_analyses( + None, + [ + { + 'lint': { 'label': 'Tris' }, + 'verts': [1,2,3,4], + 'edges': [], + 'faces': [], + }, + ]), + 'When there was no previous analysis') + self.assertEqual( + 'Found Tris: 2 edges, ' +\ + 'Nonmanifold Elements: 4 verts, 1 face', + MeshLintContinuousChecker.diff_analyses( + [ + { 'lint': { 'label': 'Tris' }, + 'verts': [], 'edges': [1,4], 'faces': [], }, + { 'lint': { 'label': 'CheckB' }, + 'verts': [], 'edges': [2,3], 'faces': [], }, + { 'lint': { 'label': 'Nonmanifold Elements' }, + 'verts': [], 'edges': [], 'faces': [2,3], }, + ], + [ + { 'lint': { 'label': 'Tris' }, + 'verts': [], 'edges': [1,4,5,6], 'faces': [], }, + { 'lint': { 'label': 'CheckB' }, + 'verts': [], 'edges': [2,3], 'faces': [], }, + { 'lint': { 'label': 'Nonmanifold Elements' }, + 'verts': [1,2,3,4], 'edges': [], + 'faces': [2,3,5], }, + ]), + 'Complex comparison of analyses') + self.assertEqual( + 'Found Tris: 1 vert, Ngons: 2 faces, ' + + 'Nonmanifold Elements: 2 edges', + MeshLintContinuousChecker.diff_analyses( + [ + { 'lint': { 'label': '6+-edge Poles' }, + 'verts': [], 'edges': [2,3], 'faces': [], }, + { 'lint': { 'label': 'Nonmanifold Elements' }, + 'verts': [], 'edges': [2,3], 'faces': [], }, + ], + [ + { 'lint': { 'label': 'Tris' }, + 'verts': [55], 'edges': [], 'faces': [], }, + { 'lint': { 'label': 'Ngons' }, + 'verts': [], 'edges': [], 'faces': [5,6], }, + { 'lint': { 'label': 'Nonmanifold Elements' }, + 'verts': [], 'edges': [2,3,4,5], 'faces': [], }, + ]), + 'User picked a different set of checks since last run.') + + + class MockBlenderObject: + def __init__(self, name, scale=Vector([1,1,1])): + self.name = name + self.scale = scale + + + class TestUI(unittest.TestCase): + def test_complaints(self): + f = MESH_PT_MeshLintControl.build_object_criticisms + self.assertEqual([], f([], 0), 'Nothing selected') + self.assertEqual( + [], + f([MockBlenderObject('lsmft')], 0), + 'Ok name') + self.assertEqual( + ['...but "Cube" is not a great name.'], + f([MockBlenderObject('Cube')], 0), + 'Bad name, otherwise problem-free.') + self.assertEqual( + [], + f([MockBlenderObject('Hassenfrass')], 12), + 'Good name, but with problems.') + self.assertEqual( + ['...and also "Cube" is not a great name.'], + f([MockBlenderObject('Cube')], 23), + 'Bad name, and problems, too.') + self.assertEqual( + [ + '...but "Sphere" is not a great name.', + '...and also "Cube" is not a great name.' + ], + f([ + MockBlenderObject('Sphere'), + MockBlenderObject('Cube') ], 0), + 'Two bad names.') + + scaled = MockBlenderObject('Solartech', scale=Vector([.2,2,1])) + self.assertEqual( + [ '...but "Solartech" has an unapplied scale.' ], + f([scaled], 0), + 'Only problem is unapplied scale.' + ) + + class QuietOnSuccessTestResult(unittest.TextTestResult): + def startTest(self, test): + pass + + def addSuccess(self, test): + pass + + + class QuietTestRunner(unittest.TextTestRunner): + resultclass = QuietOnSuccessTestResult + + # Ugh. I really shouldn't have to include this much code, but they + # left it so unrefactored I don't know what else to do. My other + # option is to override the stream and substitute out the success + # case, but that's a mess, too. - rking + def run(self, test): + "Run the given test case or test suite." + result = self._makeResult() + unittest.registerResult(result) + result.failfast = self.failfast + result.buffer = self.buffer + with warnings.catch_warnings(): + if self.warnings: + # if self.warnings is set, use it to filter all the + # warnings + warnings.simplefilter(self.warnings) + # if the filter is 'default' or 'always', special-case + # the warnings from the deprecated unittest methods to + # show them no more than once per module, because they + # can be fairly noisy. The -Wd and -Wa flags can be + # used to bypass this only when self.warnings is None. + if self.warnings in ['default', 'always']: + warnings.filterwarnings('module', + category=DeprecationWarning, + message='Please use assert\w+ instead.') + startTime = time.time() + startTestRun = getattr(result, 'startTestRun', None) + if startTestRun is not None: + startTestRun() + try: + test(result) + finally: + stopTestRun = getattr(result, 'stopTestRun', None) + if stopTestRun is not None: + stopTestRun() + stopTime = time.time() + timeTaken = stopTime - startTime + result.printErrors() + run = result.testsRun + + expectedFails = unexpectedSuccesses = skipped = 0 + try: + results = map(len, (result.expectedFailures, + result.unexpectedSuccesses, + result.skipped)) + except AttributeError: + pass + else: + expectedFails, unexpectedSuccesses, skipped = results + + infos = [] + if not result.wasSuccessful(): + self.stream.write("FAILED") + failed, errored = len(result.failures), len(result.errors) + if failed: + infos.append("failures=%d" % failed) + if errored: + infos.append("errors=%d" % errored) + if skipped: + infos.append("skipped=%d" % skipped) + if expectedFails: + infos.append("expected failures=%d" % expectedFails) + if unexpectedSuccesses: + infos.append( + "unexpected successes=%d" % unexpectedSuccesses) + return result + + if __name__ == '__main__': + unittest.main( + testRunner=QuietTestRunner, + argv=['dummy'], + exit=False, + verbosity=0) + + except ImportError: + print( + "MeshLint complains over missing unittest module.", """ + No harm, but it is odd. If you want to send a message to + rking@panoptic.com describing your system, he'd like to track down + this condition.""") + + + + classes = ( + # QuietTestRunner, + # QuietOnSuccessTestResult, + # TestUI,MockBlenderObject, + # TestAnalysis, + # TestUtilities, + # TestControl, + MESH_PT_MeshLintControl, + MeshLintObjectDeselector, + MeshLintSelector, + # MeshLintObjectLooper, + MeshLintVitalizer, + # MeshLintContinuousChecker, + # MeshLintAnalyzer + ) + + + def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + + + + def unregister(): + from bpy.utils import unregister_class + + for cls in classes: + unregister_class(cls) + + + if __name__ == "__main__": + register() +except: + # OK, I totally don't get why this is necessary. But otherwise I am not + # seeing error text. Causes the extra indent over all above code. =( + import sys + exc = sys.exc_info() + print("MeshLint Oops: ", exc[1], exc[2]) + +# vim:ts=4 sw=4 sts=4