diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..12d3040 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +extend-ignore = E111, E114, E501, E722, E121, E203, W503 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..7a58ca8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +f16bd51ebf0783ddd477a865dabd32d6576594e9 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..090d5bd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +--- +repos: + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 7.1.0 + hooks: + - id: flake8 + - repo: https://github.com/PyCQA/pylint.git + rev: v3.2.5 + hooks: + - id: pylint + name: pylint + language_version: python3 + additional_dependencies: + - typing_extensions + args: + - --load-plugins=pylint.extensions.redefined_variable_type,pylint.extensions.bad_builtin + - --disable=import-error + - repo: https://github.com/google/yamlfmt + rev: v0.13.0 + hooks: + - id: yamlfmt + args: + - -conf + - .yamlfmt diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..fa443f3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,6 @@ +[MESSAGES CONTROL] +disable=bad-indentation,missing-class-docstring,missing-module-docstring,missing-function-docstring,invalid-name,fixme,line-too-long,duplicate-code,unspecified-encoding,consider-using-f-string,consider-using-with,broad-exception-raised + +extension-pkg-whitelist=math,zlib,struct +# Temporary, until https://github.com/PyCQA/pylint/issues/4297 is resolved +generated-members=struct.* diff --git a/bin/analyze_tilt.py b/bin/analyze_tilt.py index d4cc40c..d7df10c 100755 --- a/bin/analyze_tilt.py +++ b/bin/analyze_tilt.py @@ -3,10 +3,10 @@ import openbrush.tilt -if __name__ == '__main__': +if __name__ == "__main__": path = " ".join(sys.argv[1:]) try: tilt = openbrush.tilt.Tilt(path) - pprint(tilt.metadata['CameraPaths']) + pprint(tilt.metadata["CameraPaths"]) except Exception as e: print("ERROR: %s" % e) diff --git a/bin/concatenate_tilt.py b/bin/concatenate_tilt.py index f5c1ae8..b5867cf 100644 --- a/bin/concatenate_tilt.py +++ b/bin/concatenate_tilt.py @@ -15,7 +15,7 @@ def destroy(filename): def increment_timestamp(stroke, increment): """Adds *increment* to all control points in stroke.""" - timestamp_idx = stroke.cp_ext_lookup['timestamp'] + timestamp_idx = stroke.cp_ext_lookup["timestamp"] for cp in stroke.controlpoints: cp.extension[timestamp_idx] += increment @@ -26,16 +26,20 @@ def merge_metadata_from_tilt(tilt_dest, tilt_source): - ModelIndex - ImageIndex""" with tilt_dest.mutable_metadata() as md: - to_append = set(tilt_source.metadata['BrushIndex']) - set(md['BrushIndex']) - md['BrushIndex'].extend(sorted(to_append)) + to_append = set(tilt_source.metadata["BrushIndex"]) - set(md["BrushIndex"]) + md["BrushIndex"].extend(sorted(to_append)) - if 'ImageIndex' in tilt_source.metadata: - tilt_dest.metadata['ImageIndex'] = tilt_dest.metadata.get('ImageIndex', []) + \ - tilt_source.metadata['ImageIndex'] + if "ImageIndex" in tilt_source.metadata: + tilt_dest.metadata["ImageIndex"] = ( + tilt_dest.metadata.get("ImageIndex", []) + + tilt_source.metadata["ImageIndex"] + ) - if 'ModelIndex' in tilt_source.metadata: - tilt_dest.metadata['ModelIndex'] = tilt_dest.metadata.get('ModelIndex', []) + \ - tilt_source.metadata['ModelIndex'] + if "ModelIndex" in tilt_source.metadata: + tilt_dest.metadata["ModelIndex"] = ( + tilt_dest.metadata.get("ModelIndex", []) + + tilt_source.metadata["ModelIndex"] + ) def concatenate(file_1, file_2, file_out): @@ -50,18 +54,20 @@ def concatenate(file_1, file_2, file_out): merge_metadata_from_tilt(tilt_out, tilt_2) tilt_out._guid_to_idx = dict( - (guid, index) - for (index, guid) in enumerate(tilt_out.metadata['BrushIndex'])) + (guid, index) for (index, guid) in enumerate(tilt_out.metadata["BrushIndex"]) + ) final_stroke = tilt_out.sketch.strokes[-1] - final_timestamp = final_stroke.get_cp_extension(final_stroke.controlpoints[-1], 'timestamp') - timestamp_offset = final_timestamp + .03 + final_timestamp = final_stroke.get_cp_extension( + final_stroke.controlpoints[-1], "timestamp" + ) + timestamp_offset = final_timestamp + 0.03 for stroke in tilt_2.sketch.strokes: copy = stroke.clone() # Convert brush index to one that works for tilt_out - stroke_guid = tilt_2.metadata['BrushIndex'][stroke.brush_idx] + stroke_guid = tilt_2.metadata["BrushIndex"][stroke.brush_idx] copy.brush_idx = tilt_out._guid_to_idx[stroke_guid] tilt_out.sketch.strokes.append(copy) @@ -75,15 +81,25 @@ def concatenate(file_1, file_2, file_out): def main(): import argparse + parser = argparse.ArgumentParser( - usage='%(prog)s -f FILE1 -f FILE2 ... -o OUTPUT_FILE' + usage="%(prog)s -f FILE1 -f FILE2 ... -o OUTPUT_FILE" + ) + parser.add_argument( + "-f", + dest="files", + metavar="FILE", + action="append", + required=True, + help="A file to concatenate. May pass multiple times", + ) + parser.add_argument( + "-o", + metavar="OUTPUT_FILE", + dest="output_file", + required=True, + help="The name of the output file", ) - parser.add_argument('-f', dest='files', metavar='FILE', action='append', - required=True, - help='A file to concatenate. May pass multiple times') - parser.add_argument('-o', metavar='OUTPUT_FILE', dest='output_file', - required=True, - help='The name of the output file') args = parser.parse_args() if len(args.files) < 2: parser.error("Pass at least two files") @@ -94,5 +110,5 @@ def main(): print("Wrote", args.output_file) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/bin/dump_tilt.py b/bin/dump_tilt.py index 27d2764..289e404 100755 --- a/bin/dump_tilt.py +++ b/bin/dump_tilt.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,8 +22,11 @@ import sys try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) from openbrush.tilt import Tilt except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) @@ -34,8 +37,10 @@ def dump_sketch(sketch): """Prints out some rough information about the strokes. Pass a openbrush.tilt.Sketch instance.""" cooky, version, unused = sketch.header[0:3] - print('Cooky:0x%08x Version:%s Unused:%s Extra:(%d bytes)' % ( - cooky, version, unused, len(sketch.additional_header))) + print( + "Cooky:0x%08x Version:%s Unused:%s Extra:(%d bytes)" + % (cooky, version, unused, len(sketch.additional_header)) + ) # Create dicts that are the union of all the stroke-extension and # control-point-extension # lookup tables. @@ -45,55 +50,64 @@ def dump_sketch(sketch): union_stroke_extension.update(stroke.stroke_ext_lookup) union_cp_extension.update(stroke.cp_ext_lookup) - print("Stroke Ext: %s" % ', '.join(list(union_stroke_extension.keys()))) - print("CPoint Ext: %s" % ', '.join(list(union_cp_extension.keys()))) + print("Stroke Ext: %s" % ", ".join(list(union_stroke_extension.keys()))) + print("CPoint Ext: %s" % ", ".join(list(union_cp_extension.keys()))) - for (i, stroke) in enumerate(sketch.strokes): - print("%3d: " % i, end=' ') + for i, stroke in enumerate(sketch.strokes): + print("%3d: " % i, end=" ") dump_stroke(stroke) def dump_stroke(stroke): """Prints out some information about the stroke.""" - if len(stroke.controlpoints) and 'timestamp' in stroke.cp_ext_lookup: + if len(stroke.controlpoints) and "timestamp" in stroke.cp_ext_lookup: cp = stroke.controlpoints[0] - timestamp = stroke.cp_ext_lookup['timestamp'] - start_ts = ' t:%6.1f' % (cp.extension[timestamp] * .001) + timestamp = stroke.cp_ext_lookup["timestamp"] + start_ts = " t:%6.1f" % (cp.extension[timestamp] * 0.001) else: - start_ts = '' + start_ts = "" try: - scale = stroke.extension[stroke.stroke_ext_lookup['scale']] + scale = stroke.extension[stroke.stroke_ext_lookup["scale"]] except KeyError: scale = 1 - if 'group' in stroke.stroke_ext_lookup: - group = stroke.extension[stroke.stroke_ext_lookup['group']] + if "group" in stroke.stroke_ext_lookup: + group = stroke.extension[stroke.stroke_ext_lookup["group"]] else: - group = '--' + group = "--" - if 'seed' in stroke.stroke_ext_lookup: - seed = '%08x' % stroke.extension[stroke.stroke_ext_lookup['seed']] + if "seed" in stroke.stroke_ext_lookup: + seed = "%08x" % stroke.extension[stroke.stroke_ext_lookup["seed"]] else: - seed = '-none-' - - print("B:%2d S:%.3f C:#%02X%02X%02X g:%2s s:%8s %s [%4d]" % ( - stroke.brush_idx, stroke.brush_size * scale, - int(stroke.brush_color[0] * 255), - int(stroke.brush_color[1] * 255), - int(stroke.brush_color[2] * 255), - # stroke.brush_color[3], - group, seed, - start_ts, - len(stroke.controlpoints))) + seed = "-none-" + + print( + "B:%2d S:%.3f C:#%02X%02X%02X g:%2s s:%8s %s [%4d]" + % ( + stroke.brush_idx, + stroke.brush_size * scale, + int(stroke.brush_color[0] * 255), + int(stroke.brush_color[1] * 255), + int(stroke.brush_color[2] * 255), + # stroke.brush_color[3], + group, + seed, + start_ts, + len(stroke.controlpoints), + ) + ) def main(): import argparse + parser = argparse.ArgumentParser(description="View information about a .tilt") - parser.add_argument('--strokes', action='store_true', help="Dump the sketch strokes") - parser.add_argument('--metadata', action='store_true', help="Dump the metadata") - parser.add_argument('files', type=str, nargs='+', help="Files to examine") + parser.add_argument( + "--strokes", action="store_true", help="Dump the sketch strokes" + ) + parser.add_argument("--metadata", action="store_true", help="Dump the metadata") + parser.add_argument("files", type=str, nargs="+", help="Files to examine") args = parser.parse_args() if not (args.strokes or args.metadata): @@ -107,5 +121,5 @@ def main(): pprint.pprint(t.metadata) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/bin/geometry_json_to_fbx.py b/bin/geometry_json_to_fbx.py index 0339784..543ca6a 100755 --- a/bin/geometry_json_to_fbx.py +++ b/bin/geometry_json_to_fbx.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,10 +16,10 @@ # Historical sample code that converts Tilt Brush '.json' exports to .fbx. # This script is superseded by Tilt Brush native .fbx exports. -# +# # There are command-line options to fine-tune the fbx creation. # The defaults are: -# +# # - Weld vertices # - Join strokes using the same brush into a single mesh # - Don't create backface geometry for single-sided brushes""" @@ -30,19 +30,22 @@ from itertools import groupby try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) from openbrush.export import iter_meshes, TiltBrushMesh, SINGLE_SIDED_FLAT_BRUSH except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) sys.exit(1) -arch = 'x64' if '64' in platform.architecture()[0] else 'x86' -dir = 'c:/Program Files/Autodesk/FBX/FBX Python SDK' +arch = "x64" if "64" in platform.architecture()[0] else "x86" +dir = "c:/Program Files/Autodesk/FBX/FBX Python SDK" versions = sorted(os.listdir(dir), reverse=True) found = False for version in versions: - path = '{0}/{1}/lib/Python27_{2}'.format(dir, version, arch) + path = "{0}/{1}/lib/Python27_{2}".format(dir, version, arch) if os.path.exists(path): sys.path.append(path) try: @@ -54,18 +57,24 @@ sys.exit(1) break if not found: - print("Please install the Python FBX SDK: http://www.autodesk.com/products/fbx/", file=sys.stderr) + print( + "Please install the Python FBX SDK: http://www.autodesk.com/products/fbx/", + file=sys.stderr, + ) # ---------------------------------------------------------------------- # Utils # ---------------------------------------------------------------------- + def as_fvec4(tup, scale=1): if len(tup) == 3: return FbxVector4(tup[0] * scale, tup[1] * scale, tup[2] * scale) else: - return FbxVector4(tup[0] * scale, tup[1] * scale, tup[2] * scale, tup[3] * scale) + return FbxVector4( + tup[0] * scale, tup[1] * scale, tup[2] * scale, tup[3] * scale + ) def as_fvec2(tup): @@ -76,10 +85,10 @@ def as_fcolor(abgr_int, memo={}): try: return memo[abgr_int] except KeyError: - a = (abgr_int >> 24) & 0xff - b = (abgr_int >> 16) & 0xff - g = (abgr_int >> 8) & 0xff - r = (abgr_int) & 0xff + a = (abgr_int >> 24) & 0xFF + b = (abgr_int >> 16) & 0xFF + g = (abgr_int >> 8) & 0xFF + r = (abgr_int) & 0xFF scale = 1.0 / 255.0 memo[abgr_int] = val = FbxColor(r * scale, g * scale, b * scale, a * scale) return val @@ -89,16 +98,18 @@ def as_fcolor(abgr_int, memo={}): # Export # ---------------------------------------------------------------------- + def write_fbx_meshes(meshes, outf_name): """Emit a TiltBrushMesh as a .fbx file""" import FbxCommon + (sdk, scene) = FbxCommon.InitializeSdkObjects() - docInfo = FbxDocumentInfo.Create(sdk, 'DocInfo') - docInfo.Original_ApplicationVendor.Set('Google') - docInfo.Original_ApplicationName.Set('Tilt Brush') - docInfo.LastSaved_ApplicationVendor.Set('Google') - docInfo.LastSaved_ApplicationName.Set('Tilt Brush') + docInfo = FbxDocumentInfo.Create(sdk, "DocInfo") + docInfo.Original_ApplicationVendor.Set("Google") + docInfo.Original_ApplicationName.Set("Tilt Brush") + docInfo.LastSaved_ApplicationVendor.Set("Google") + docInfo.LastSaved_ApplicationName.Set("Tilt Brush") scene.SetDocumentInfo(docInfo) for mesh in meshes: @@ -107,8 +118,9 @@ def write_fbx_meshes(meshes, outf_name): FbxCommon.SaveScene(sdk, scene, outf_name) -def create_fbx_layer(fbx_mesh, data, converter_fn, layer_class, - allow_index=False, allow_allsame=False): +def create_fbx_layer( + fbx_mesh, data, converter_fn, layer_class, allow_index=False, allow_allsame=False +): """Returns an instance of layer_class populated with the passed data, or None if the passed data is empty/nonexistent. @@ -141,7 +153,7 @@ def create_fbx_layer(fbx_mesh, data, converter_fn, layer_class, layer_elt.SetReferenceMode(FbxLayerElement.eDirect) if len(unique_data) == 1: direct.Add(converter_fn(unique_data[0])) - elif allow_index and len(unique_data) <= len(data) * .7: + elif allow_index and len(unique_data) <= len(data) * 0.7: layer_elt.SetMappingMode(FbxLayerElement.eByControlPoint) layer_elt.SetReferenceMode(FbxLayerElement.eIndexToDirect) for datum in unique_data: @@ -163,7 +175,7 @@ def create_fbx_layer(fbx_mesh, data, converter_fn, layer_class, def add_mesh_to_scene(sdk, scene, mesh): """Emit a TiltBrushMesh as a .fbx file""" - name = mesh.name or 'Tilt Brush' + name = mesh.name or "Tilt Brush" # Todo: pass scene instead? fbx_mesh = FbxMesh.Create(sdk, name) @@ -176,43 +188,47 @@ def add_mesh_to_scene(sdk, scene, mesh): for i, v in enumerate(mesh.v): fbx_mesh.SetControlPointAt(as_fvec4(v, scale=100), i) - layer_elt = create_fbx_layer( - fbx_mesh, mesh.n, as_fvec4, FbxLayerElementNormal) + layer_elt = create_fbx_layer(fbx_mesh, mesh.n, as_fvec4, FbxLayerElementNormal) if layer_elt is not None: layer0.SetNormals(layer_elt) layer_elt = create_fbx_layer( - fbx_mesh, mesh.c, as_fcolor, FbxLayerElementVertexColor, + fbx_mesh, + mesh.c, + as_fcolor, + FbxLayerElementVertexColor, allow_index=True, - allow_allsame=True) + allow_allsame=True, + ) if layer_elt is not None: layer0.SetVertexColors(layer_elt) # Tilt Brush may have 3- or 4-element UV channels, and may have multiple # UV channels. This only handles the standard case of 2-component UVs layer_elt = create_fbx_layer( - fbx_mesh, mesh.uv0, as_fvec2, FbxLayerElementUV, - allow_index=True) + fbx_mesh, mesh.uv0, as_fvec2, FbxLayerElementUV, allow_index=True + ) if layer_elt is not None: layer0.SetUVs(layer_elt, FbxLayerElement.eTextureDiffuse) pass layer_elt = create_fbx_layer( - fbx_mesh, mesh.t, as_fvec4, FbxLayerElementTangent, - allow_index=True) + fbx_mesh, mesh.t, as_fvec4, FbxLayerElementTangent, allow_index=True + ) if layer_elt is not None: layer0.SetTangents(layer_elt) # Unity's FBX import requires Binormals to be present in order to import the # tangents but doesn't actually use them, so we just output some dummy data. layer_elt = create_fbx_layer( - fbx_mesh, ((0, 0, 0, 0),), as_fvec4, FbxLayerElementBinormal, - allow_allsame=True) + fbx_mesh, ((0, 0, 0, 0),), as_fvec4, FbxLayerElementBinormal, allow_allsame=True + ) if layer_elt is not None: layer0.SetBinormals(layer_elt) layer_elt = create_fbx_layer( - fbx_mesh, (), lambda x: x, FbxLayerElementMaterial, allow_allsame=True) + fbx_mesh, (), lambda x: x, FbxLayerElementMaterial, allow_allsame=True + ) if layer_elt is not None: layer0.SetMaterials(layer_elt) @@ -240,34 +256,60 @@ def add_mesh_to_scene(sdk, scene, mesh): # main # ---------------------------------------------------------------------- + def main(): import argparse - parser = argparse.ArgumentParser(description="""Converts Tilt Brush '.json' exports to .fbx.""") - parser.add_argument('filename', help="Exported .json files to convert to fbx") - grp = parser.add_argument_group(description="Merging and optimization") - grp.add_argument('--merge-stroke', action='store_true', - help="Merge all strokes into a single mesh") - grp.add_argument('--merge-brush', action='store_true', - help="(default) Merge strokes that use the same brush into a single mesh") - grp.add_argument('--no-merge-brush', action='store_false', dest='merge_brush', - help="Turn off --merge-brush") - - grp.add_argument('--weld-verts', action='store_true', - help="(default) Weld vertices") - grp.add_argument('--no-weld-verts', action='store_false', dest='weld_verts', - help="Turn off --weld-verts") - - parser.add_argument('--add-backface', action='store_true', - help="Add backfaces to strokes that don't have them") - - parser.add_argument('-o', dest='output_filename', metavar='FILE', - help="Name of output file; defaults to .fbx") + parser = argparse.ArgumentParser( + description="""Converts Tilt Brush '.json' exports to .fbx.""" + ) + parser.add_argument("filename", help="Exported .json files to convert to fbx") + grp = parser.add_argument_group(description="Merging and optimization") + grp.add_argument( + "--merge-stroke", + action="store_true", + help="Merge all strokes into a single mesh", + ) + + grp.add_argument( + "--merge-brush", + action="store_true", + help="(default) Merge strokes that use the same brush into a single mesh", + ) + grp.add_argument( + "--no-merge-brush", + action="store_false", + dest="merge_brush", + help="Turn off --merge-brush", + ) + + grp.add_argument( + "--weld-verts", action="store_true", help="(default) Weld vertices" + ) + grp.add_argument( + "--no-weld-verts", + action="store_false", + dest="weld_verts", + help="Turn off --weld-verts", + ) + + parser.add_argument( + "--add-backface", + action="store_true", + help="Add backfaces to strokes that don't have them", + ) + + parser.add_argument( + "-o", + dest="output_filename", + metavar="FILE", + help="Name of output file; defaults to .fbx", + ) parser.set_defaults(merge_brush=True, weld_verts=True) args = parser.parse_args() if args.output_filename is None: - args.output_filename = os.path.splitext(args.filename)[0] + '.fbx' + args.output_filename = os.path.splitext(args.filename)[0] + ".fbx" meshes = list(iter_meshes(args.filename)) for mesh in meshes: @@ -276,23 +318,26 @@ def main(): mesh.add_backface() if args.merge_stroke: - meshes = [TiltBrushMesh.from_meshes(meshes, name='strokes')] + meshes = [TiltBrushMesh.from_meshes(meshes, name="strokes")] elif args.merge_brush: + def by_guid(m): return (m.brush_guid, m.brush_name) - meshes = [TiltBrushMesh.from_meshes(list(group), name='All %s' % (key[1],)) - for (key, group) in groupby(sorted(meshes, key=by_guid), key=by_guid)] + meshes = [ + TiltBrushMesh.from_meshes(list(group), name="All %s" % (key[1],)) + for (key, group) in groupby(sorted(meshes, key=by_guid), key=by_guid) + ] if args.weld_verts: for mesh in meshes: # We don't write out tangents, so it's safe to ignore them when welding - mesh.collapse_verts(ignore=('t',)) + mesh.collapse_verts(ignore=("t",)) mesh.remove_degenerate() write_fbx_meshes(meshes, args.output_filename) print("Wrote", args.output_filename) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/bin/geometry_json_to_obj.py b/bin/geometry_json_to_obj.py index 5b9263a..e272257 100755 --- a/bin/geometry_json_to_obj.py +++ b/bin/geometry_json_to_obj.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,14 +16,14 @@ # Historical sample code that converts Tilt Brush '.json' exports to .obj. # This script is superseded by Tilt Brush native .fbx exports. -# +# # There are various possible ways you might want the .obj file converted: -# +# # - Should the entire sketch be converted to a single mesh? Or all # strokes that use the same brush? Or maybe one mesh per stroke? # - Should backfaces be kept or removed? # - Should vertices be welded? How aggressively? -# +# # This sample keeps backfaces, merges all strokes into a single mesh, # and does no vertex welding. It can also be easily customized to do any # of the above. @@ -32,8 +32,11 @@ import sys try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) from openbrush.export import iter_meshes, TiltBrushMesh, SINGLE_SIDED_FLAT_BRUSH except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) @@ -44,13 +47,14 @@ def write_obj(mesh, outf_name, use_color): """Emits a TiltBrushMesh as a .obj file. If use_color, emit vertex color as a non-standard .obj extension.""" from io import StringIO + tmpf = StringIO() if use_color: for v, c32 in zip(mesh.v, mesh.c): - r = ((c32 >> 0) & 0xff) / 255.0 - g = ((c32 >> 8) & 0xff) / 255.0 - b = ((c32 >> 16) & 0xff) / 255.0 + r = ((c32 >> 0) & 0xFF) / 255.0 + g = ((c32 >> 8) & 0xFF) / 255.0 + b = ((c32 >> 16) & 0xFF) / 255.0 tmpf.write("v %f %f %f %f %f %f\n" % (v[0], v[1], v[2], r, g, b)) tmpf.write("vc %f %f %f\n" % (r, g, b)) else: @@ -75,49 +79,70 @@ def write_obj(mesh, outf_name, use_color): tmpf.write("vn 0 0 0\n") if has_n and has_uv: - for (t1, t2, t3) in mesh.tri: - t1 += 1; - t2 += 1; + for t1, t2, t3 in mesh.tri: + t1 += 1 + t2 += 1 t3 += 1 - tmpf.write("f %d/%d/%d %d/%d/%d %d/%d/%d\n" % (t1, t1, t1, t2, t2, t2, t3, t3, t3)) + tmpf.write( + "f %d/%d/%d %d/%d/%d %d/%d/%d\n" % (t1, t1, t1, t2, t2, t2, t3, t3, t3) + ) elif has_n: - for (t1, t2, t3) in mesh.tri: - t1 += 1; - t2 += 1; + for t1, t2, t3 in mesh.tri: + t1 += 1 + t2 += 1 t3 += 1 tmpf.write("f %d//%d %d//%d %d//%d\n" % (t1, t1, t2, t2, t3, t3)) elif has_uv: - for (t1, t2, t3) in mesh.tri: - t1 += 1; - t2 += 1; + for t1, t2, t3 in mesh.tri: + t1 += 1 + t2 += 1 t3 += 1 tmpf.write("f %d/%d %d/%d %d/%d\n" % (t1, t1, t2, t2, t3, t3)) else: - for (t1, t2, t3) in mesh.tri: - t1 += 1; - t2 += 1; + for t1, t2, t3 in mesh.tri: + t1 += 1 + t2 += 1 t3 += 1 tmpf.write("f %d %d %d\n" % (t1, t2, t3)) - with file(outf_name, 'wb') as outf: + with file(outf_name, "wb") as outf: outf.write(tmpf.getvalue()) def main(): import argparse - parser = argparse.ArgumentParser(description="Converts Tilt Brush '.json' exports to .obj.") - parser.add_argument('filename', help="Exported .json files to convert to obj") - parser.add_argument('--cooked', action='store_true', dest='cooked', default=True, - help="(default) Strip geometry of normals, weld verts, and give single-sided triangles corresponding backfaces.") - parser.add_argument('--color', action='store_true', - help="Add vertex color to 'v' and 'vc' elements. WARNING: May produce incompatible .obj files.") - parser.add_argument('--raw', action='store_false', dest='cooked', - help="Emit geometry just as it comes from Tilt Brush. Depending on the brush, triangles may not have backfaces, adjacent triangles will mostly not share verts.") - parser.add_argument('-o', dest='output_filename', metavar='FILE', - help="Name of output file; defaults to .obj") + + parser = argparse.ArgumentParser( + description="Converts Tilt Brush '.json' exports to .obj." + ) + parser.add_argument("filename", help="Exported .json files to convert to obj") + parser.add_argument( + "--cooked", + action="store_true", + dest="cooked", + default=True, + help="(default) Strip geometry of normals, weld verts, and give single-sided triangles corresponding backfaces.", + ) + parser.add_argument( + "--color", + action="store_true", + help="Add vertex color to 'v' and 'vc' elements. WARNING: May produce incompatible .obj files.", + ) + parser.add_argument( + "--raw", + action="store_false", + dest="cooked", + help="Emit geometry just as it comes from Tilt Brush. Depending on the brush, triangles may not have backfaces, adjacent triangles will mostly not share verts.", + ) + parser.add_argument( + "-o", + dest="output_filename", + metavar="FILE", + help="Name of output file; defaults to .obj", + ) args = parser.parse_args() if args.output_filename is None: - args.output_filename = os.path.splitext(args.filename)[0] + '.obj' + args.output_filename = os.path.splitext(args.filename)[0] + ".obj" meshes = list(iter_meshes(args.filename)) for mesh in meshes: @@ -128,7 +153,7 @@ def main(): if mesh.brush_guid in SINGLE_SIDED_FLAT_BRUSH: mesh.add_backfaces() mesh = TiltBrushMesh.from_meshes(meshes) - mesh.collapse_verts(ignore=('uv0', 'uv1', 'c', 't')) + mesh.collapse_verts(ignore=("uv0", "uv1", "c", "t")) mesh.remove_degenerate() else: mesh = TiltBrushMesh.from_meshes(meshes) @@ -137,5 +162,5 @@ def main(): print("Wrote", args.output_filename) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/bin/normalize_sketch.py b/bin/normalize_sketch.py index 2a929e2..2c93ab8 100755 --- a/bin/normalize_sketch.py +++ b/bin/normalize_sketch.py @@ -25,8 +25,11 @@ import sys try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) from openbrush.tilt import Tilt except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) @@ -40,7 +43,8 @@ def _quaternion_multiply_quaternion(q0, q1): w0 * x1 + x0 * w1 + y0 * z1 - z0 * y1, w0 * y1 + y0 * w1 + z0 * x1 - x0 * z1, w0 * z1 + z0 * w1 + x0 * y1 - y0 * x1, - w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1] + w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1, + ] def _quaternion_conjugate(q): @@ -50,7 +54,9 @@ def _quaternion_conjugate(q): def _quaternion_multiply_vector(q, v): qv = v + [0] - return _quaternion_multiply_quaternion(_quaternion_multiply_quaternion(q, qv), _quaternion_conjugate(q))[:3] + return _quaternion_multiply_quaternion( + _quaternion_multiply_quaternion(q, qv), _quaternion_conjugate(q) + )[:3] def _transform_point(scene_translation, scene_rotation, scene_scale, pos): @@ -61,14 +67,18 @@ def _transform_point(scene_translation, scene_rotation, scene_scale, pos): def _adjust_guide(scene_translation, scene_rotation, scene_scale, guide): - guide['Extents'] = [scene_scale * b for b in guide['Extents']] - _adjust_transform(scene_translation, scene_rotation, scene_scale, guide['Transform']) + guide["Extents"] = [scene_scale * b for b in guide["Extents"]] + _adjust_transform( + scene_translation, scene_rotation, scene_scale, guide["Transform"] + ) def _adjust_transform(scene_translation, scene_rotation, scene_scale, transform): scaledTranslation = [scene_scale * b for b in transform[0]] rotatedTranslation = _quaternion_multiply_vector(scene_rotation, scaledTranslation) - translatedTranslation = [b + a for a, b in zip(scene_translation, rotatedTranslation)] + translatedTranslation = [ + b + a for a, b in zip(scene_translation, rotatedTranslation) + ] transform[0] = translatedTranslation transform[1] = _quaternion_multiply_quaternion(scene_rotation, transform[1]) @@ -76,13 +86,13 @@ def _adjust_transform(scene_translation, scene_rotation, scene_scale, transform) def normalize_tilt_file(tilt_file): - scene_translation = tilt_file.metadata['SceneTransformInRoomSpace'][0] - scene_rotation = tilt_file.metadata['SceneTransformInRoomSpace'][1] - scene_scale = tilt_file.metadata['SceneTransformInRoomSpace'][2] + scene_translation = tilt_file.metadata["SceneTransformInRoomSpace"][0] + scene_rotation = tilt_file.metadata["SceneTransformInRoomSpace"][1] + scene_scale = tilt_file.metadata["SceneTransformInRoomSpace"][2] # Normalize strokes for stroke in tilt_file.sketch.strokes: - if stroke.has_stroke_extension('scale'): + if stroke.has_stroke_extension("scale"): stroke.scale *= scene_scale else: stroke.scale = scene_scale @@ -90,74 +100,85 @@ def normalize_tilt_file(tilt_file): pos = cp.position pos = _transform_point(scene_translation, scene_rotation, scene_scale, pos) cp.position = pos - cp.orientation = _quaternion_multiply_quaternion(scene_rotation, cp.orientation) + cp.orientation = _quaternion_multiply_quaternion( + scene_rotation, cp.orientation + ) with tilt_file.mutable_metadata() as metadata: # Reset scene transform to be identity. - metadata['SceneTransformInRoomSpace'][0] = [0., 0., 0.] - metadata['SceneTransformInRoomSpace'][1] = [0., 0., 0., 1.] - metadata['SceneTransformInRoomSpace'][2] = 1. + metadata["SceneTransformInRoomSpace"][0] = [0.0, 0.0, 0.0] + metadata["SceneTransformInRoomSpace"][1] = [0.0, 0.0, 0.0, 1.0] + metadata["SceneTransformInRoomSpace"][2] = 1.0 # Adjust guide transforms to match. - if 'GuideIndex' in metadata: - for guide_type in metadata['GuideIndex']: - for guide in guide_type['States']: + if "GuideIndex" in metadata: + for guide_type in metadata["GuideIndex"]: + for guide in guide_type["States"]: _adjust_guide(scene_translation, scene_rotation, scene_scale, guide) # Adjust model transforms to match. - if 'ModelIndex' in metadata: - for model_type in metadata['ModelIndex']: - for transform in model_type['Transforms']: - _adjust_transform(scene_translation, scene_rotation, scene_scale, transform) + if "ModelIndex" in metadata: + for model_type in metadata["ModelIndex"]: + for transform in model_type["Transforms"]: + _adjust_transform( + scene_translation, scene_rotation, scene_scale, transform + ) # Adjust image transforms to match. - if 'ImageIndex' in metadata: - for image_type in metadata['ImageIndex']: - for transform in image_type['Transforms']: - _adjust_transform(scene_translation, scene_rotation, scene_scale, transform) + if "ImageIndex" in metadata: + for image_type in metadata["ImageIndex"]: + for transform in image_type["Transforms"]: + _adjust_transform( + scene_translation, scene_rotation, scene_scale, transform + ) # Adjust lights to match. - if 'Lights' in metadata: - metadata['Lights']['Shadow']['Orientation'] = _quaternion_multiply_quaternion(scene_rotation, - metadata['Lights']['Shadow'][ - 'Orientation']) - metadata['Lights']['NoShadow']['Orientation'] = _quaternion_multiply_quaternion(scene_rotation, - metadata['Lights'][ - 'NoShadow'][ - 'Orientation']) + if "Lights" in metadata: + metadata["Lights"]["Shadow"]["Orientation"] = ( + _quaternion_multiply_quaternion( + scene_rotation, metadata["Lights"]["Shadow"]["Orientation"] + ) + ) + metadata["Lights"]["NoShadow"]["Orientation"] = ( + _quaternion_multiply_quaternion( + scene_rotation, metadata["Lights"]["NoShadow"]["Orientation"] + ) + ) # Adjust environment to match. - if 'Environment' in metadata: - metadata['Environment']['FogDensity'] /= scene_scale - metadata['Environment']['GradientSkew'] = _quaternion_multiply_quaternion(scene_rotation, - metadata['Environment'][ - 'GradientSkew']) + if "Environment" in metadata: + metadata["Environment"]["FogDensity"] /= scene_scale + metadata["Environment"]["GradientSkew"] = _quaternion_multiply_quaternion( + scene_rotation, metadata["Environment"]["GradientSkew"] + ) # u'Mirror' and u'ThumbnailCameraTransformInRoomSpace' are in room space so don't need to be normalized. def main(): import argparse - parser = argparse.ArgumentParser(description= - "Create a normalized version of the sketch (with 'Normalized' appended to\ + + parser = argparse.ArgumentParser( + description="Create a normalized version of the sketch (with 'Normalized' appended to\ the file name) which is scaled, rotated, and translated so that resetting the\ transform will bring you back to the initial size, orientation, and position.\ - But the environment size, orientation, and position will also be reset.") - parser.add_argument('files', type=str, nargs='+', help="Sketches to normalize") + But the environment size, orientation, and position will also be reset." + ) + parser.add_argument("files", type=str, nargs="+", help="Sketches to normalize") args = parser.parse_args() for filename in args.files: name, ext = os.path.splitext(filename) - filename_normalized = name + 'Normalized' + ext + filename_normalized = name + "Normalized" + ext shutil.copy(filename, filename_normalized) tilt_file = Tilt(filename_normalized) normalize_tilt_file(tilt_file) tilt_file.write_sketch() - print('WARNING: Environment position has changed in ' + filename + '.') + print("WARNING: Environment position has changed in " + filename + ".") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/bin/tilt_to_strokes_dae.py b/bin/tilt_to_strokes_dae.py index 6cacb12..d0aab84 100755 --- a/bin/tilt_to_strokes_dae.py +++ b/bin/tilt_to_strokes_dae.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,8 +20,11 @@ import xml.etree.ElementTree as ET try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) from openbrush.tilt import Tilt except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) @@ -60,68 +63,104 @@ def __init__(self): self.next_ids = collections.defaultdict(int) self.root = ET.Element( - 'COLLADA', + "COLLADA", xmlns="http://www.collada.org/2008/03/COLLADASchema", - version="1.5.0") + version="1.5.0", + ) self.tree = ET.ElementTree(self.root) self._init_asset() - self.library_effects = ET.SubElement(self.root, 'library_effects') - self.library_materials = ET.SubElement(self.root, 'library_materials') - self.library_geometries = ET.SubElement(self.root, 'library_geometries') - self.library_visual_scenes = ET.SubElement(self.root, 'library_visual_scenes') + self.library_effects = ET.SubElement(self.root, "library_effects") + self.library_materials = ET.SubElement(self.root, "library_materials") + self.library_geometries = ET.SubElement(self.root, "library_geometries") + self.library_visual_scenes = ET.SubElement(self.root, "library_visual_scenes") self.material = self._init_material() self.visual_scene = self._init_scene() def _init_asset(self): import datetime + now = datetime.datetime.now() self.root.append( - Element('asset', children=[ - Element('contributor', children=[ - Element('authoring_tool', text='Tilt Brush COLLADA stroke converter') - ]), - Element('created', text=now.isoformat()), - Element('modified', text=now.isoformat()), - Element('unit', meter='.1', name='decimeter'), - Element('up_axis', text='Y_UP') - ]) + Element( + "asset", + children=[ + Element( + "contributor", + children=[ + Element( + "authoring_tool", + text="Tilt Brush COLLADA stroke converter", + ) + ], + ), + Element("created", text=now.isoformat()), + Element("modified", text=now.isoformat()), + Element("unit", meter=".1", name="decimeter"), + Element("up_axis", text="Y_UP"), + ], + ) ) def _init_material(self): - effect = ET.SubElement(self.library_effects, 'effect', id=self.make_id('effect_')) + effect = ET.SubElement( + self.library_effects, "effect", id=self.make_id("effect_") + ) effect.append( - Element('profile_COMMON', children=[ - Element('technique', sid='COMMON', children=[ - Element('blinn', children=[ - Element('diffuse', children=[ - Element('color', text='0.8 0.8 0.8 1'), - ]), - Element('specular', children=[ - Element('color', text='0.2 0.2 0.2 1'), - ]), - Element('shininess', children=[Element('float', text='0.5')]) - ]) - ]) - ]) + Element( + "profile_COMMON", + children=[ + Element( + "technique", + sid="COMMON", + children=[ + Element( + "blinn", + children=[ + Element( + "diffuse", + children=[ + Element("color", text="0.8 0.8 0.8 1"), + ], + ), + Element( + "specular", + children=[ + Element("color", text="0.2 0.2 0.2 1"), + ], + ), + Element( + "shininess", + children=[Element("float", text="0.5")], + ), + ], + ) + ], + ) + ], + ) ) material = ET.SubElement( - self.library_materials, 'material', id=self.make_id('material_'), - name="Mat") - ET.SubElement(material, 'instance_effect', url='#' + effect.get('id')) + self.library_materials, "material", id=self.make_id("material_"), name="Mat" + ) + ET.SubElement(material, "instance_effect", url="#" + effect.get("id")) return material def _init_scene(self): - visual_scene = ET.SubElement(self.library_visual_scenes, 'visual_scene', - id=self.make_id('scene_')) + visual_scene = ET.SubElement( + self.library_visual_scenes, "visual_scene", id=self.make_id("scene_") + ) self.root.append( - Element('scene', children=[ - Element('instance_visual_scene', url='#' + visual_scene.get('id')) - ]) + Element( + "scene", + children=[ + Element("instance_visual_scene", url="#" + visual_scene.get("id")) + ], + ) ) return visual_scene - def make_id(self, prefix='ID'): + def make_id(self, prefix="ID"): val = self.next_ids[prefix] self.next_ids[prefix] += 1 new_id = prefix + str(val) @@ -130,7 +169,7 @@ def make_id(self, prefix='ID'): def write(self, filename): header = '\n' _indent(self.root) - with file(filename, 'wb') as outf: + with file(filename, "wb") as outf: outf.write(header) self.tree.write(outf) @@ -159,80 +198,138 @@ def iter_positions(stroke): assert len(raw_floats) % 3 == 0 - geom_id = self.make_id('stroke_') - source_id = geom_id + '_src' - floats_id = geom_id + '_fs' - verts_id = geom_id + '_vs' + geom_id = self.make_id("stroke_") + source_id = geom_id + "_src" + floats_id = geom_id + "_fs" + verts_id = geom_id + "_vs" - geometry = ET.SubElement(self.library_geometries, 'geometry', id=geom_id) + geometry = ET.SubElement(self.library_geometries, "geometry", id=geom_id) geometry.append( - Element('mesh', children=[ - Element('source', id=source_id, children=[ - Element('float_array', id=floats_id, - count=str(len(raw_floats)), - text=' '.join(map(str, raw_floats))), - Element('technique_common', children=[ - Element('accessor', - count=str(len(raw_floats) / 3), stride='3', - source='#' + floats_id, + Element( + "mesh", + children=[ + Element( + "source", + id=source_id, + children=[ + Element( + "float_array", + id=floats_id, + count=str(len(raw_floats)), + text=" ".join(map(str, raw_floats)), + ), + Element( + "technique_common", children=[ - Element('param', name='X', type='float'), - Element('param', name='Y', type='float'), - Element('param', name='Z', type='float') - ]) - ]) - ]), - Element('vertices', id=verts_id, children=[ - Element('input', semantic='POSITION', source='#' + source_id) - ]), - Element('linestrips', count='1', material='Material1', children=[ - Element('input', offset='0', semantic='VERTEX', set='0', source='#' + verts_id), - Element('p', text=' '.join(map(str, range(len(raw_floats) / 3)))) - ]) - ]) + Element( + "accessor", + count=str(len(raw_floats) / 3), + stride="3", + source="#" + floats_id, + children=[ + Element("param", name="X", type="float"), + Element("param", name="Y", type="float"), + Element("param", name="Z", type="float"), + ], + ) + ], + ), + ], + ), + Element( + "vertices", + id=verts_id, + children=[ + Element( + "input", semantic="POSITION", source="#" + source_id + ) + ], + ), + Element( + "linestrips", + count="1", + material="Material1", + children=[ + Element( + "input", + offset="0", + semantic="VERTEX", + set="0", + source="#" + verts_id, + ), + Element( + "p", text=" ".join(map(str, range(len(raw_floats) / 3))) + ), + ], + ), + ], + ) ) return geometry def _add_stroke_node(self, geometry): - name = 'Spline.' + geometry.get('id') + name = "Spline." + geometry.get("id") self.visual_scene.append( - Element('node', id=self.make_id('node_'), name=name, children=[ - Element('instance_geometry', url='#' + geometry.get('id'), children=[ - Element('bind_material', children=[ - Element('technique_common', children=[ - Element('instance_material', symbol='Material1', - target='#' + self.material.get('id'), - children=[ - Element('bind_vertex_input', - semantic='UVSET0', - input_semantic='TEXCOORD', - input_set='0') - ]) - ]) - ]) - ]) - ]) + Element( + "node", + id=self.make_id("node_"), + name=name, + children=[ + Element( + "instance_geometry", + url="#" + geometry.get("id"), + children=[ + Element( + "bind_material", + children=[ + Element( + "technique_common", + children=[ + Element( + "instance_material", + symbol="Material1", + target="#" + self.material.get("id"), + children=[ + Element( + "bind_vertex_input", + semantic="UVSET0", + input_semantic="TEXCOORD", + input_set="0", + ) + ], + ) + ], + ) + ], + ) + ], + ) + ], + ) ) def main(args): import argparse - parser = argparse.ArgumentParser(description="Converts .tilt files to a Collada .dae containing spline data.") - parser.add_argument('files', type=str, nargs='*', help="Files to convert to dae") + + parser = argparse.ArgumentParser( + description="Converts .tilt files to a Collada .dae containing spline data." + ) + parser.add_argument("files", type=str, nargs="*", help="Files to convert to dae") args = parser.parse_args(args) for filename in args.files: t = Tilt(filename) - outf_name = os.path.splitext(os.path.basename(filename))[0] + '.dae' + outf_name = os.path.splitext(os.path.basename(filename))[0] + ".dae" dae = ColladaFile() for stroke in t.sketch.strokes: dae.add_stroke(stroke) dae.write(outf_name) - print('Wrote', outf_name) + print("Wrote", outf_name) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/bin/unpack_tilt.py b/bin/unpack_tilt.py index 680498c..fe0f396 100755 --- a/bin/unpack_tilt.py +++ b/bin/unpack_tilt.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,8 +18,11 @@ import sys try: - sys.path.append(os.path.join(os.path.dirname(os.path.dirname( - os.path.abspath(__file__))), 'Python')) + sys.path.append( + os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Python" + ) + ) import openbrush.unpack except ImportError: print("Please put the 'Python' directory in your PYTHONPATH", file=sys.stderr) @@ -39,12 +42,16 @@ def convert(in_name, compress): def main(): import argparse + parser = argparse.ArgumentParser( - description="Converts .tilt files from packed format (zip) to unpacked format (directory), optionally applying compression.") - parser.add_argument('files', type=str, nargs='+', - help="Files to convert to the other format") - parser.add_argument('--compress', action='store_true', - help="Use compression (default: off)") + description="Converts .tilt files from packed format (zip) to unpacked format (directory), optionally applying compression." + ) + parser.add_argument( + "files", type=str, nargs="+", help="Files to convert to the other format" + ) + parser.add_argument( + "--compress", action="store_true", help="Use compression (default: off)" + ) args = parser.parse_args() for arg in args.files: try: @@ -53,5 +60,5 @@ def main(): print("ERROR: %s" % e) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/openbrush/__init__.py b/openbrush/__init__.py index d75e91a..b6e5c41 100644 --- a/openbrush/__init__.py +++ b/openbrush/__init__.py @@ -1,11 +1,11 @@ # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/openbrush/__version__.py b/openbrush/__version__.py index 7d7c530..32db95f 100644 --- a/openbrush/__version__.py +++ b/openbrush/__version__.py @@ -1,6 +1,6 @@ from pkg_resources import get_distribution, DistributionNotFound try: - __version__ = get_distribution('openbrush').version + __version__ = get_distribution("openbrush").version except DistributionNotFound: - __version__ = '0.0.0' + __version__ = "0.0.0" diff --git a/openbrush/export.py b/openbrush/export.py index 0818294..207c104 100644 --- a/openbrush/export.py +++ b/openbrush/export.py @@ -1,11 +1,11 @@ # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,16 +24,18 @@ class TiltBrushMesh""" from itertools import zip_longest from uuid import UUID -SINGLE_SIDED_FLAT_BRUSH = set([ - UUID("cb92b597-94ca-4255-b017-0e3f42f12f9e"), # Fire - UUID("cf019139-d41c-4eb0-a1d0-5cf54b0a42f3"), # Highlighter - UUID("e8ef32b1-baa8-460a-9c2c-9cf8506794f5"), # Hypercolor - UUID("2241cd32-8ba2-48a5-9ee7-2caef7e9ed62"), # Light - UUID("c33714d1-b2f9-412e-bd50-1884c9d46336"), # Plasma - UUID("ad1ad437-76e2-450d-a23a-e17f8310b960"), # Rainbow - UUID("44bb800a-fbc3-4592-8426-94ecb05ddec3"), # Streamers - UUID("d229d335-c334-495a-a801-660ac8a87360"), # Velvet Ink -]) +SINGLE_SIDED_FLAT_BRUSH = set( + [ + UUID("cb92b597-94ca-4255-b017-0e3f42f12f9e"), # Fire + UUID("cf019139-d41c-4eb0-a1d0-5cf54b0a42f3"), # Highlighter + UUID("e8ef32b1-baa8-460a-9c2c-9cf8506794f5"), # Hypercolor + UUID("2241cd32-8ba2-48a5-9ee7-2caef7e9ed62"), # Light + UUID("c33714d1-b2f9-412e-bd50-1884c9d46336"), # Plasma + UUID("ad1ad437-76e2-450d-a23a-e17f8310b960"), # Rainbow + UUID("44bb800a-fbc3-4592-8426-94ecb05ddec3"), # Streamers + UUID("d229d335-c334-495a-a801-660ac8a87360"), # Velvet Ink + ] +) def _grouper(n, iterable, fillvalue=None): @@ -44,11 +46,11 @@ def _grouper(n, iterable, fillvalue=None): def iter_meshes(filename): """Given a Tilt Brush .json export, yields TiltBrushMesh instances.""" - obj = json.load(file(filename, 'rb')) - lookup = obj['brushes'] + obj = json.load(file(filename, "rb")) + lookup = obj["brushes"] for dct in lookup: - dct['guid'] = UUID(dct['guid']) - for json_stroke in obj['strokes']: + dct["guid"] = UUID(dct["guid"]) + for json_stroke in obj["strokes"]: yield TiltBrushMesh._from_json(json_stroke, lookup) @@ -67,14 +69,15 @@ class TiltBrushMesh(object): .tri list of triangles (3-tuples of ints) """ + VERTEX_ATTRIBUTES = [ # Attribute name, type code - ('v', 'f', None), - ('n', 'f', 3), - ('uv0', 'f', None), - ('uv1', 'f', None), - ('c', 'I', 1), - ('t', 'f', 4), + ("v", "f", None), + ("n", "f", 3), + ("uv0", "f", None), + ("uv1", "f", None), + ("c", "I", 1), + ("t", "f", 4), ] @classmethod @@ -83,9 +86,9 @@ def _from_json(cls, obj, brush_lookup): empty = None stroke = TiltBrushMesh() - brush = brush_lookup[obj['brush']] - stroke.brush_name = brush['name'] - stroke.brush_guid = UUID(str(brush['guid'])) + brush = brush_lookup[obj["brush"]] + stroke.brush_name = brush["name"] + stroke.brush_guid = UUID(str(brush["guid"])) # Vertex attributes # If stroke is non-empty, 'v' is always present, and always comes first @@ -98,11 +101,13 @@ def _from_json(cls, obj, brush_lookup): else: fmt = "<%d%c" % (len(data_bytes) / 4, typechar) data_words = struct.unpack(fmt, data_bytes) - if attr == 'v': + if attr == "v": num_verts = len(data_words) / 3 assert (len(data_words) % num_verts) == 0 stride_words = len(data_words) / num_verts - assert (expected_stride is None) or (stride_words == expected_stride) + assert (expected_stride is None) or ( + stride_words == expected_stride + ) if stride_words > 1: data_grouped = list(_grouper(stride_words, data_words)) else: @@ -111,12 +116,14 @@ def _from_json(cls, obj, brush_lookup): else: # For convenience, fill in with an empty array if empty is None: - empty = [None, ] * num_verts + empty = [ + None, + ] * num_verts setattr(stroke, attr, empty) # Triangle indices. 'tri' might not exist, if empty - if 'tri' in obj: - data_bytes = base64.b64decode(obj['tri']) + if "tri" in obj: + data_bytes = base64.b64decode(obj["tri"]) data_words = struct.unpack("<%dI" % (len(data_bytes) / 4), data_bytes) assert len(data_words) % 3 == 0 stroke.tri = list(_grouper(3, data_words)) @@ -150,8 +157,9 @@ def from_meshes(cls, strokes, name=None): dest.uv1.extend(stroke.uv1) dest.c.extend(stroke.c) dest.t.extend(stroke.t) - dest.tri.extend([(t[0] + offset, t[1] + offset, t[2] + offset) - for t in stroke.tri]) + dest.tri.extend( + [(t[0] + offset, t[1] + offset, t[2] + offset) for t in stroke.tri] + ) return dest def __init__(self): @@ -165,11 +173,11 @@ def collapse_verts(self, ignore=None): Put triangle indices into a canonical order, with lowest index first. *ignore* is a list of attribute names to ignore when comparing.""" # Convert from SOA to AOS - compare = set(('n', 'uv0', 'uv1', 'c', 't')) + compare = set(("n", "uv0", "uv1", "c", "t")) if ignore is not None: compare -= set(ignore) compare = sorted(compare) - compare.insert(0, 'v') + compare.insert(0, "v") struct_of_arrays = [] for attr_name in sorted(compare): @@ -221,7 +229,8 @@ def add_backfaces(self): num_verts = len(self.v) def flip_vec3(val): - if val is None: return None + if val is None: + return None return (-val[0], -val[1], -val[2]) # Duplicate vert data, flipping normals @@ -235,9 +244,9 @@ def flip_vec3(val): more_tris = [] for tri in self.tri: - more_tris.append((num_verts + tri[0], - num_verts + tri[2], - num_verts + tri[1])) + more_tris.append( + (num_verts + tri[0], num_verts + tri[2], num_verts + tri[1]) + ) self.tri += more_tris def remove_backfaces(self): @@ -277,11 +286,14 @@ def recenter(self): self.v[i] = (v[0] - a0, v[1] - a1, v[2] - a2) def dump(self, verbose=False): - print(" Brush: %s, %d verts, %d tris" % (self.brush_guid, len(self.v), len(self.tri) / 3)) + print( + " Brush: %s, %d verts, %d tris" + % (self.brush_guid, len(self.v), len(self.tri) / 3) + ) if verbose: - print(' v') + print(" v") for v in self.v: - print(' ', v) - print(' t') + print(" ", v) + print(" t") for t in self.tri: - print(' ', t) + print(" ", t) diff --git a/openbrush/tilt.py b/openbrush/tilt.py index 948e851..2f3834c 100644 --- a/openbrush/tilt.py +++ b/openbrush/tilt.py @@ -1,11 +1,11 @@ # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,29 +22,38 @@ import uuid from io import StringIO -__all__ = ('Tilt', 'Sketch', 'Stroke', 'ControlPoint', - 'BadTilt', 'BadMetadata', 'MissingKey') +__all__ = ( + "Tilt", + "Sketch", + "Stroke", + "ControlPoint", + "BadTilt", + "BadMetadata", + "MissingKey", +) # Format characters are as for struct.pack/unpack, with the addition of # '@' which is a 4-byte-length-prefixed data blob. STROKE_EXTENSION_BITS = { - 0x1: ('flags', 'I'), - 0x2: ('scale', 'f'), - 0x4: ('group', 'I'), - 0x8: ('seed', 'I'), - 'unknown': lambda bit: ('stroke_ext_%d' % math.log(bit, 2), - 'I' if (bit & 0xffff) else '@') + 0x1: ("flags", "I"), + 0x2: ("scale", "f"), + 0x4: ("group", "I"), + 0x8: ("seed", "I"), + "unknown": lambda bit: ( + "stroke_ext_%d" % math.log(bit, 2), + "I" if (bit & 0xFFFF) else "@", + ), } STROKE_EXTENSION_BY_NAME = dict( (info[0], (bit, info[1])) for (bit, info) in STROKE_EXTENSION_BITS.items() - if bit != 'unknown' + if bit != "unknown" ) CONTROLPOINT_EXTENSION_BITS = { - 0x1: ('pressure', 'f'), - 0x2: ('timestamp', 'I'), - 'unknown': lambda bit: ('cp_ext_%d' % math.log(bit, 2), 'I') + 0x1: ("pressure", "f"), + 0x2: ("timestamp", "I"), + "unknown": lambda bit: ("cp_ext_%d" % math.log(bit, 2), "I"), } @@ -52,6 +61,7 @@ # Internal utils # + class memoized_property(object): """Modeled after @property, but runs the getter exactly once""" @@ -81,7 +91,7 @@ def write(self, data): return self.inf.write(data) def read_length_prefixed(self): - n, = self.unpack("value to name->value - name_to_value = dict((name, self.extension[idx]) - for (name, idx) in self.stroke_ext_lookup.items()) + name_to_value = dict( + (name, self.extension[idx]) + for (name, idx) in self.stroke_ext_lookup.items() + ) name_to_value[name] = value bit, exttype = STROKE_EXTENSION_BY_NAME[name] self.stroke_mask |= bit - _, self.stroke_ext_writer, self.stroke_ext_lookup = \ - _make_stroke_ext_reader(self.stroke_mask) + _, self.stroke_ext_writer, self.stroke_ext_lookup = _make_stroke_ext_reader( + self.stroke_mask + ) # Convert back to idx->value self.extension = [None] * len(self.stroke_ext_lookup) - for (name, idx) in self.stroke_ext_lookup.items(): + for name, idx in self.stroke_ext_lookup.items(): self.extension[idx] = name_to_value[name] def delete_stroke_extension(self, name): @@ -533,18 +579,21 @@ def delete_stroke_extension(self, name): idx = self.stroke_ext_lookup[name] # Convert from idx->value to name->value - name_to_value = dict((name, self.extension[idx]) - for (name, idx) in self.stroke_ext_lookup.items()) + name_to_value = dict( + (name, self.extension[idx]) + for (name, idx) in self.stroke_ext_lookup.items() + ) del name_to_value[name] bit, exttype = STROKE_EXTENSION_BY_NAME[name] self.stroke_mask &= ~bit - _, self.stroke_ext_writer, self.stroke_ext_lookup = \ - _make_stroke_ext_reader(self.stroke_mask) + _, self.stroke_ext_writer, self.stroke_ext_lookup = _make_stroke_ext_reader( + self.stroke_mask + ) # Convert back to idx->value self.extension = [None] * len(self.stroke_ext_lookup) - for (name, idx) in self.stroke_ext_lookup.items(): + for name, idx in self.stroke_ext_lookup.items(): self.extension[idx] = name_to_value[name] def has_cp_extension(self, name): @@ -578,8 +627,8 @@ def _write(self, b): class ControlPoint(object): """Data for a single control point from a stroke. Attributes: - .position Position as 3 floats. Units are decimeters. - .orientation Orientation of controller as a quaternion (x, y, z, w).""" + .position Position as 3 floats. Units are decimeters. + .orientation Orientation of controller as a quaternion (x, y, z, w).""" @classmethod def from_file(cls, b, cp_ext_reader): @@ -593,12 +642,12 @@ def from_file(cls, b, cp_ext_reader): def clone(self): inst = self.__class__() - for attr in ('position', 'orientation', 'extension'): + for attr in ("position", "orientation", "extension"): setattr(inst, attr, list(getattr(self, attr))) return inst def _write(self, b, cp_ext_writer): - p = self.position; + p = self.position o = self.orientation b.pack("<7f", p[0], p[1], p[2], o[0], o[1], o[2], o[3]) cp_ext_writer(b, self.extension) diff --git a/openbrush/unpack.py b/openbrush/unpack.py index 82f2ac8..6fe1d38 100644 --- a/openbrush/unpack.py +++ b/openbrush/unpack.py @@ -1,11 +1,11 @@ # Copyright 2016 Google Inc. All Rights Reserved. -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,23 +20,24 @@ import zipfile from io import StringIO -__all__ = ('ConversionError', 'convert_zip_to_dir', 'convert_dir_to_zip') +__all__ = ("ConversionError", "convert_zip_to_dir", "convert_dir_to_zip") -HEADER_FMT = '<4sHH' -HEADER_V1_FMT = HEADER_FMT + 'II' +HEADER_FMT = "<4sHH" +HEADER_V1_FMT = HEADER_FMT + "II" STANDARD_FILE_ORDER = [ - 'header.bin', - 'thumbnail.png', - 'metadata.json', - 'main.json', - 'data.sketch' + "header.bin", + "thumbnail.png", + "metadata.json", + "main.json", + "data.sketch", ] STANDARD_FILE_ORDER = dict((n, i) for (i, n) in enumerate(STANDARD_FILE_ORDER)) class ConversionError(Exception): """An error occurred in the zip <-> directory conversion process""" + pass @@ -44,11 +45,13 @@ def _destroy(file_or_dir): """Ensure that *file_or_dir* does not exist in the filesystem, deleting it if necessary.""" import stat + if os.path.isfile(file_or_dir): os.chmod(file_or_dir, stat.S_IWRITE) os.unlink(file_or_dir) elif os.path.isdir(file_or_dir): import stat + for r, ds, fs in os.walk(file_or_dir, topdown=False): for f in fs: os.chmod(os.path.join(r, f), stat.S_IWRITE) @@ -68,7 +71,7 @@ def _read_and_check_header(inf): except struct.error as e: raise ConversionError("Unexpected header error: %s" % (e,)) - if sentinel != 'tilT': + if sentinel != "tilT": raise ConversionError("Sentinel looks weird: %r" % sentinel) more = headerSize - len(base_bytes) @@ -77,11 +80,15 @@ def _read_and_check_header(inf): more_bytes = inf.read(more) if len(more_bytes) < more: - raise ConversionError("Bad header size (claim %s, actual %s)" % (more, len(more_bytes))) + raise ConversionError( + "Bad header size (claim %s, actual %s)" % (more, len(more_bytes)) + ) zip_sentinel = inf.read(4) - if zip_sentinel != '' and zip_sentinel != 'PK\x03\x04': - raise ConversionError("Don't see zip sentinel after header: %r" % (zip_sentinel,)) + if zip_sentinel != "" and zip_sentinel != "PK\x03\x04": + raise ConversionError( + "Don't see zip sentinel after header: %r" % (zip_sentinel,) + ) if headerVersion != 1: raise ConversionError("Bogus version %s" % headerVersion) @@ -90,11 +97,11 @@ def _read_and_check_header(inf): def convert_zip_to_dir(in_name): """Returns True if compression was used""" - with file(in_name, 'rb') as inf: + with file(in_name, "rb") as inf: header_bytes = _read_and_check_header(inf) compression = False - out_name = in_name + '._part' + out_name = in_name + "._part" if os.path.exists(out_name): raise ConversionError("Remove %s first" % out_name) @@ -106,10 +113,10 @@ def convert_zip_to_dir(in_name): if member.compress_size != member.file_size: compression = True zf.extract(member, out_name) - with file(os.path.join(out_name, 'header.bin'), 'wb') as outf: + with file(os.path.join(out_name, "header.bin"), "wb") as outf: outf.write(header_bytes) - tmp = in_name + '._prev' + tmp = in_name + "._prev" os.rename(in_name, tmp) os.rename(out_name, in_name) _destroy(tmp) @@ -121,7 +128,7 @@ def convert_zip_to_dir(in_name): def convert_dir_to_zip(in_name, compress): in_name = os.path.normpath(in_name) # remove trailing '/' if any - out_name = in_name + '.part' + out_name = in_name + ".part" if os.path.exists(out_name): raise ConversionError("Remove %s first" % out_name) @@ -130,14 +137,17 @@ def by_standard_order(filename): try: idx = STANDARD_FILE_ORDER[lfile] except KeyError: - raise ConversionError("Unknown file %s; this is probably not a .tilt" % filename) + raise ConversionError( + "Unknown file %s; this is probably not a .tilt" % filename + ) return (idx, lfile) # Make sure metadata.json looks like valid utf-8 (rather than latin-1 # or something else that will cause mojibake) try: - with file(os.path.join(in_name, 'metadata.json')) as inf: + with file(os.path.join(in_name, "metadata.json")) as inf: import json + json.load(inf) except IOError as e: raise ConversionError("Cannot validate metadata.json: %s" % e) @@ -151,29 +161,31 @@ def by_standard_order(filename): header_bytes = None zipf = StringIO() - with zipfile.ZipFile(zipf, 'a', compression, False) as zf: - for (r, ds, fs) in os.walk(in_name): + with zipfile.ZipFile(zipf, "a", compression, False) as zf: + for r, ds, fs in os.walk(in_name): fs.sort(key=by_standard_order) for f in fs: fullf = os.path.join(r, f) - if f == 'header.bin': + if f == "header.bin": header_bytes = file(fullf).read() continue - arcname = fullf[len(in_name) + 1:] + arcname = fullf[len(in_name) + 1 :] zf.write(fullf, arcname, compression) if header_bytes is None: print("Missing header; using default") - header_bytes = struct.pack(HEADER_V1_FMT, 'tilT', struct.calcsize(HEADER_V1_FMT), 1, 0, 0) + header_bytes = struct.pack( + HEADER_V1_FMT, "tilT", struct.calcsize(HEADER_V1_FMT), 1, 0, 0 + ) if not _read_and_check_header(StringIO(header_bytes)): raise ConversionError("Invalid header.bin") - with file(out_name, 'wb') as outf: + with file(out_name, "wb") as outf: outf.write(header_bytes) outf.write(zipf.getvalue()) - tmp = in_name + '._prev' + tmp = in_name + "._prev" os.rename(in_name, tmp) os.rename(out_name, in_name) _destroy(tmp) diff --git a/setup.py b/setup.py index 22abba9..646fc54 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,19 @@ from setuptools import setup, find_packages setup( - name='openbrush', + name="openbrush", use_scm_version=True, - setup_requires=['setuptools_scm'], + setup_requires=["setuptools_scm"], packages=find_packages(), scripts=[ - 'bin/analyze_tilt.py', - 'bin/concatenate_tilt.py', - 'bin/dump_tilt.py', - 'bin/geometry_json_to_fbx.py', - 'bin/geometry_json_to_obj.py', - 'bin/normalize_sketch.py', - 'bin/tilt_to_strokes_dae.py', - 'bin/unpack_tilt.py' + "bin/analyze_tilt.py", + "bin/concatenate_tilt.py", + "bin/dump_tilt.py", + "bin/geometry_json_to_fbx.py", + "bin/geometry_json_to_obj.py", + "bin/normalize_sketch.py", + "bin/tilt_to_strokes_dae.py", + "bin/unpack_tilt.py", ], install_requires=[ # Add your package dependencies here @@ -21,11 +21,11 @@ include_package_data=True, zip_safe=False, classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], - python_requires='>=3.9', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', + python_requires=">=3.9", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", ) diff --git a/tests/test_tilt.py b/tests/test_tilt.py index 14d39b9..0f7c10c 100644 --- a/tests/test_tilt.py +++ b/tests/test_tilt.py @@ -21,87 +21,92 @@ @contextlib.contextmanager -def copy_of_tilt(tilt_file='data/sketch1.tilt', as_filename=False): - """Returns a mutate-able copy of tilt_file, and removes it when done.""" - base = os.path.abspath(os.path.dirname(__file__)) - full_filename = os.path.join(base, tilt_file) - tmp_filename = os.path.splitext(full_filename)[0] + '_tmp.tilt' - shutil.copy(src=full_filename, dst=tmp_filename) - try: - if as_filename: - yield tmp_filename - else: - yield Tilt(tmp_filename) - finally: - if os.path.exists(tmp_filename): - os.unlink(tmp_filename) +def copy_of_tilt(tilt_file="data/sketch1.tilt", as_filename=False): + """Returns a mutate-able copy of tilt_file, and removes it when done.""" + base = os.path.abspath(os.path.dirname(__file__)) + full_filename = os.path.join(base, tilt_file) + tmp_filename = os.path.splitext(full_filename)[0] + "_tmp.tilt" + shutil.copy(src=full_filename, dst=tmp_filename) + try: + if as_filename: + yield tmp_filename + else: + yield Tilt(tmp_filename) + finally: + if os.path.exists(tmp_filename): + os.unlink(tmp_filename) def as_float32(f): - import struct - return struct.unpack('f', struct.pack('f', f))[0] + import struct + + return struct.unpack("f", struct.pack("f", f))[0] class TestTiltMutations(unittest.TestCase): - def test_as_directory(self): - # Test Tilt.as_directory - with copy_of_tilt(as_filename=True) as tilt_filename: - with Tilt.as_directory(tilt_filename): - self.assertTrue(os.path.isdir(tilt_filename)) - self.assertTrue(os.path.exists(os.path.join(tilt_filename, 'metadata.json'))) - - def test_can_mutate_metadata(self): - import uuid - random_guid = str(uuid.uuid4()) - with copy_of_tilt() as tilt: - with tilt.mutable_metadata() as dct: - # Check that they are different references - dct['EnvironmentPreset'] = random_guid - self.assertNotEqual( - tilt.metadata['EnvironmentPreset'], dct['EnvironmentPreset']) - # Check that it's copied back on exit from mutable_metadata - self.assertEqual(tilt.metadata['EnvironmentPreset'], random_guid) - # Check that the mutations persist - tilt2 = Tilt(tilt.filename) - self.assertEqual(tilt2.metadata['EnvironmentPreset'], random_guid) - - def test_can_del_sketch(self): - # Test that "del tilt.sketch" forces it to re-load from disk - with copy_of_tilt() as tilt: - stroke = tilt.sketch.strokes[0] - del tilt.sketch - stroke2 = tilt.sketch.strokes[0] - assert stroke is not stroke2 - - def test_mutate_control_point(self): - # Test that control point mutations are saved - with copy_of_tilt() as tilt: - stroke = tilt.sketch.strokes[0] - new_y = as_float32(stroke.controlpoints[0].position[1] + 3) - stroke.controlpoints[0].position[1] = new_y - tilt.write_sketch() - del tilt.sketch - self.assertEqual(tilt.sketch.strokes[0].controlpoints[0].position[1], new_y) - - def test_stroke_extension(self): - # Test that control point extensions can be added and removed - with copy_of_tilt() as tilt: - stroke = tilt.sketch.strokes[0] - # This sketch was made before stroke scale was a thing - self.assertEqual(stroke.flags, 0) - self.assertRaises(AttributeError, (lambda: stroke.scale)) - # Test adding some extension data - stroke.scale = 1.25 - self.assertEqual(stroke.scale, 1.25) - # Test removing extension data - del stroke.flags - self.assertRaises(AttributeError (lambda: stroke.flags)) - # Test that the changes survive a save+load - tilt.write_sketch() - stroke2 = Tilt(tilt.filename).sketch.strokes[0] - self.assertEqual(stroke2.scale, 1.25) - self.assertRaises(AttributeError (lambda: stroke2.flags)) - - -if __name__ == '__main__': - unittest.main() + def test_as_directory(self): + # Test Tilt.as_directory + with copy_of_tilt(as_filename=True) as tilt_filename: + with Tilt.as_directory(tilt_filename): + self.assertTrue(os.path.isdir(tilt_filename)) + self.assertTrue( + os.path.exists(os.path.join(tilt_filename, "metadata.json")) + ) + + def test_can_mutate_metadata(self): + import uuid + + random_guid = str(uuid.uuid4()) + with copy_of_tilt() as tilt: + with tilt.mutable_metadata() as dct: + # Check that they are different references + dct["EnvironmentPreset"] = random_guid + self.assertNotEqual( + tilt.metadata["EnvironmentPreset"], dct["EnvironmentPreset"] + ) + # Check that it's copied back on exit from mutable_metadata + self.assertEqual(tilt.metadata["EnvironmentPreset"], random_guid) + # Check that the mutations persist + tilt2 = Tilt(tilt.filename) + self.assertEqual(tilt2.metadata["EnvironmentPreset"], random_guid) + + def test_can_del_sketch(self): + # Test that "del tilt.sketch" forces it to re-load from disk + with copy_of_tilt() as tilt: + stroke = tilt.sketch.strokes[0] + del tilt.sketch + stroke2 = tilt.sketch.strokes[0] + assert stroke is not stroke2 + + def test_mutate_control_point(self): + # Test that control point mutations are saved + with copy_of_tilt() as tilt: + stroke = tilt.sketch.strokes[0] + new_y = as_float32(stroke.controlpoints[0].position[1] + 3) + stroke.controlpoints[0].position[1] = new_y + tilt.write_sketch() + del tilt.sketch + self.assertEqual(tilt.sketch.strokes[0].controlpoints[0].position[1], new_y) + + def test_stroke_extension(self): + # Test that control point extensions can be added and removed + with copy_of_tilt() as tilt: + stroke = tilt.sketch.strokes[0] + # This sketch was made before stroke scale was a thing + self.assertEqual(stroke.flags, 0) + self.assertRaises(AttributeError, (lambda: stroke.scale)) + # Test adding some extension data + stroke.scale = 1.25 + self.assertEqual(stroke.scale, 1.25) + # Test removing extension data + del stroke.flags + self.assertRaises(AttributeError(lambda: stroke.flags)) + # Test that the changes survive a save+load + tilt.write_sketch() + stroke2 = Tilt(tilt.filename).sketch.strokes[0] + self.assertEqual(stroke2.scale, 1.25) + self.assertRaises(AttributeError(lambda: stroke2.flags)) + + +if __name__ == "__main__": + unittest.main()