Browse Source

Automated tests better. Changed to explicit datastructure for writing into file

Geoffrey 7 years ago
parent
commit
cf6bf130cd

+ 7 - 0
Makefile

@@ -1,6 +1,7 @@
 PYLINT = pylint3
 PYLINT = pylint3
 PEP8 = pep8
 PEP8 = pep8
 BLENDER = blender
 BLENDER = blender
+GODOT = godot
 
 
 pylint:
 pylint:
 	$(PYLINT) io_scene_godot
 	$(PYLINT) io_scene_godot
@@ -14,3 +15,9 @@ pep8:
 test-blends:
 test-blends:
 	rm -rf ./tests/.import  # Ensure we don't have any hangover data
 	rm -rf ./tests/.import  # Ensure we don't have any hangover data
 	$(BLENDER) -b --python ./tests/scenes/export_blends.py
 	$(BLENDER) -b --python ./tests/scenes/export_blends.py
+
+
+test-import: test-blends
+	$(GODOT) -e -q --path tests/ > log.txt 2>&1
+	@cat log.txt
+	! grep -q "ERROR" log.txt

+ 1 - 1
io_scene_godot/__init__.py

@@ -133,7 +133,7 @@ class ExportGodot(bpy.types.Operator, ExportHelper):
 
 
 
 
 def menu_func(self, context):
 def menu_func(self, context):
-    """Add to the manu"""
+    """Add to the menu"""
     self.layout.operator(ExportGodot.bl_idname, text="Godot Engine (.escn)")
     self.layout.operator(ExportGodot.bl_idname, text="Godot Engine (.escn)")
 
 
 
 

+ 3 - 6
io_scene_godot/converters/mesh.py

@@ -34,11 +34,11 @@ def export_mesh_node(escn_file, export_settings, node, parent_path):
         mesh_id = export_mesh(escn_file, export_settings, node, armature)  # We need to export the mesh
         mesh_id = export_mesh(escn_file, export_settings, node, armature)  # We need to export the mesh
 
 
         mesh_node = NodeTemplate(node.name, "MeshInstance", parent_path)
         mesh_node = NodeTemplate(node.name, "MeshInstance", parent_path)
-        mesh_node.mesh = "SubResource({})".format(mesh_id)
+        mesh_node['mesh'] = "SubResource({})".format(mesh_id)
         if not physics.has_physics(node) or not physics.is_physics_root(node):
         if not physics.has_physics(node) or not physics.is_physics_root(node):
-            mesh_node.transform = node.matrix_local
+            mesh_node['transform'] = node.matrix_local
         else:
         else:
-            mesh_node.transform = mathutils.Matrix.Identity(4)
+            mesh_node['transform'] = mathutils.Matrix.Identity(4)
         escn_file.add_node(mesh_node)
         escn_file.add_node(mesh_node)
 
 
         return parent_path + '/' + node.name
         return parent_path + '/' + node.name
@@ -295,9 +295,6 @@ class Surface:
         return surface_lines
         return surface_lines
 
 
 
 
-CMP_EPSILON = 0.0001
-
-
 def fix_vertex(vtx):
 def fix_vertex(vtx):
     """Changes a single position vector from y-up to z-up"""
     """Changes a single position vector from y-up to z-up"""
     return mathutils.Vector((vtx.x, vtx.z, -vtx.y))
     return mathutils.Vector((vtx.x, vtx.z, -vtx.y))

+ 18 - 18
io_scene_godot/converters/physics.py

@@ -68,10 +68,10 @@ def export_collision_shape(escn_file, export_settings, node, parent_path,
     col_node = NodeTemplate(col_name, "CollisionShape", parent_path)
     col_node = NodeTemplate(col_name, "CollisionShape", parent_path)
 
 
     if parent_override is None:
     if parent_override is None:
-        col_node.transform = mathutils.Matrix.Identity(4) * AXIS_CORRECT
+        col_node['transform'] = mathutils.Matrix.Identity(4) * AXIS_CORRECT
     else:
     else:
         parent_to_world = parent_override.matrix_world.inverted()
         parent_to_world = parent_override.matrix_world.inverted()
-        col_node.transform = parent_to_world * node.matrix_world
+        col_node['transform'] = parent_to_world * node.matrix_world
 
 
     rbd = node.rigid_body
     rbd = node.rigid_body
 
 
@@ -80,24 +80,24 @@ def export_collision_shape(escn_file, export_settings, node, parent_path,
 
 
     if rbd.collision_shape == "BOX":
     if rbd.collision_shape == "BOX":
         col_shape = InternalResource("BoxShape")
         col_shape = InternalResource("BoxShape")
-        col_shape.extents = mathutils.Vector(bounds/2)
+        col_shape['extents'] = mathutils.Vector(bounds/2)
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
 
 
     elif rbd.collision_shape == "SPHERE":
     elif rbd.collision_shape == "SPHERE":
         col_shape = InternalResource("SphereShape")
         col_shape = InternalResource("SphereShape")
-        col_shape.radius = max(list(bounds))/2
+        col_shape['radius'] = max(list(bounds))/2
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
 
 
     elif rbd.collision_shape == "CAPSULE":
     elif rbd.collision_shape == "CAPSULE":
         col_shape = InternalResource("CapsuleShape")
         col_shape = InternalResource("CapsuleShape")
-        col_shape.radius = max(bounds.x, bounds.y) / 2
-        col_shape.height = bounds.z - col_shape.radius * 2
+        col_shape['radius'] = max(bounds.x, bounds.y) / 2
+        col_shape['height'] = bounds.z - col_shape['radius'] * 2
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
         shape_id = escn_file.add_internal_resource(col_shape, rbd)
     # elif rbd.collision_shape == "CONVEX_HULL":
     # elif rbd.collision_shape == "CONVEX_HULL":
     #   pass
     #   pass
     elif rbd.collision_shape == "MESH":
     elif rbd.collision_shape == "MESH":
         shape_id = generate_triangle_mesh_array(
         shape_id = generate_triangle_mesh_array(
-            escn_file, export_settings, 
+            escn_file, export_settings,
             node
             node
         )
         )
 
 
@@ -105,7 +105,7 @@ def export_collision_shape(escn_file, export_settings, node, parent_path,
         logging.warning("Unable to export physics shape for %s", node.name)
         logging.warning("Unable to export physics shape for %s", node.name)
 
 
     if shape_id is not None:
     if shape_id is not None:
-        col_node.shape = "SubResource({})".format(shape_id)
+        col_node['shape'] = "SubResource({})".format(shape_id)
     escn_file.add_node(col_node)
     escn_file.add_node(col_node)
 
 
     return parent_path + "/" + col_name
     return parent_path + "/" + col_name
@@ -140,7 +140,7 @@ def generate_triangle_mesh_array(escn_file, export_settings, node):
 
 
     bpy.data.meshes.remove(mesh)
     bpy.data.meshes.remove(mesh)
 
 
-    col_shape.data = Array("PoolVector3Array(", values=vert_array)
+    col_shape['data'] = Array("PoolVector3Array(", values=vert_array)
 
 
     return escn_file.add_internal_resource(col_shape, key)
     return escn_file.add_internal_resource(col_shape, key)
 
 
@@ -163,22 +163,22 @@ def export_physics_controller(escn_file, export_settings, node, parent_path):
     phys_obj = NodeTemplate(phys_name, phys_controller, parent_path)
     phys_obj = NodeTemplate(phys_name, phys_controller, parent_path)
 
 
     #  OPTIONS FOR ALL PHYSICS TYPES
     #  OPTIONS FOR ALL PHYSICS TYPES
-    phys_obj.friction = rbd.friction
-    phys_obj.bounce = rbd.restitution
+    phys_obj['friction'] = rbd.friction
+    phys_obj['bounce'] = rbd.restitution
 
 
     col_groups = 0
     col_groups = 0
     for offset, bit in enumerate(rbd.collision_groups):
     for offset, bit in enumerate(rbd.collision_groups):
         col_groups += bit << offset
         col_groups += bit << offset
 
 
-    phys_obj.transform = node.matrix_local
-    phys_obj.collision_layer = col_groups
-    phys_obj.collision_mask = col_groups
+    phys_obj['transform'] = node.matrix_local
+    phys_obj['collision_layer'] = col_groups
+    phys_obj['collision_mask'] = col_groups
 
 
     if phys_controller == "RigidBody":
     if phys_controller == "RigidBody":
-        phys_obj.can_sleep = rbd.use_deactivation
-        phys_obj.linear_damp = rbd.linear_damping
-        phys_obj.angular_damp = rbd.angular_damping
-        phys_obj.sleeping = rbd.use_start_deactivated
+        phys_obj['can_sleep'] = rbd.use_deactivation
+        phys_obj['linear_damp'] = rbd.linear_damping
+        phys_obj['angular_damp'] = rbd.angular_damping
+        phys_obj['sleeping'] = rbd.use_start_deactivated
 
 
     escn_file.add_node(phys_obj)
     escn_file.add_node(phys_obj)
 
 

+ 20 - 20
io_scene_godot/converters/simple_nodes.py

@@ -18,7 +18,7 @@ def export_empty_node(escn_file, export_settings, node, parent_path):
     if "EMPTY" not in export_settings['object_types']:
     if "EMPTY" not in export_settings['object_types']:
         return parent_path
         return parent_path
     empty_node = NodeTemplate(node.name, "Spatial", parent_path)
     empty_node = NodeTemplate(node.name, "Spatial", parent_path)
-    empty_node.transform = node.matrix_local
+    empty_node['transform'] = node.matrix_local
     escn_file.add_node(empty_node)
     escn_file.add_node(empty_node)
 
 
     return parent_path + '/' + node.name
     return parent_path + '/' + node.name
@@ -33,17 +33,17 @@ def export_camera_node(escn_file, export_settings, node, parent_path):
     cam_node = NodeTemplate(node.name, "Camera", parent_path)
     cam_node = NodeTemplate(node.name, "Camera", parent_path)
     camera = node.data
     camera = node.data
 
 
-    cam_node.far = camera.clip_end
-    cam_node.near = camera.clip_start
+    cam_node['far'] = camera.clip_end
+    cam_node['near'] = camera.clip_start
 
 
     if camera.type == "PERSP":
     if camera.type == "PERSP":
-        cam_node.projection = 0
-        cam_node.fov = math.degrees(camera.angle)
+        cam_node['projection'] = 0
+        cam_node['fov'] = math.degrees(camera.angle)
     else:
     else:
-        cam_node.projection = 1
-        cam_node.size = camera.ortho_scale
+        cam_node['projection'] = 1
+        cam_node['size'] = camera.ortho_scale
 
 
-    cam_node.transform = node.matrix_local * AXIS_CORRECT
+    cam_node['transform'] = node.matrix_local * AXIS_CORRECT
     escn_file.add_node(cam_node)
     escn_file.add_node(cam_node)
 
 
     return parent_path + '/' + node.name
     return parent_path + '/' + node.name
@@ -60,8 +60,8 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
 
     if light.type == "POINT":
     if light.type == "POINT":
         light_node = NodeTemplate(node.name, "OmniLight", parent_path)
         light_node = NodeTemplate(node.name, "OmniLight", parent_path)
-        light_node.omni_range = light.distance
-        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+        light_node['omni_range'] = light.distance
+        light_node['shadow_enabled'] = light.shadow_method != "NOSHADOW"
 
 
         if not light.use_sphere:
         if not light.use_sphere:
             logging.warning(
             logging.warning(
@@ -70,10 +70,10 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
 
     elif light.type == "SPOT":
     elif light.type == "SPOT":
         light_node = NodeTemplate(node.name, "SpotLight", parent_path)
         light_node = NodeTemplate(node.name, "SpotLight", parent_path)
-        light_node.spot_range = light.distance
-        light_node.spot_angle = math.degrees(light.spot_size/2)
-        light_node.spot_angle_attenuation = 0.2/(light.spot_blend + 0.01)
-        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+        light_node['spot_range'] = light.distance
+        light_node['spot_angle'] = math.degrees(light.spot_size/2)
+        light_node['spot_angle_attenuation'] = 0.2/(light.spot_blend + 0.01)
+        light_node['shadow_enabled'] = light.shadow_method != "NOSHADOW"
 
 
         if not light.use_sphere:
         if not light.use_sphere:
             logging.warning(
             logging.warning(
@@ -82,7 +82,7 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
 
     elif light.type == "SUN":
     elif light.type == "SUN":
         light_node = NodeTemplate(node.name, "DirectionalLight", parent_path)
         light_node = NodeTemplate(node.name, "DirectionalLight", parent_path)
-        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+        light_node['shadow_enabled'] = light.shadow_method != "NOSHADOW"
     else:
     else:
         light_node = None
         light_node = None
         logging.warning(
         logging.warning(
@@ -91,11 +91,11 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
 
     if light_node is not None:
     if light_node is not None:
         # Properties common to all lights
         # Properties common to all lights
-        light_node.light_color = mathutils.Color(light.color)
-        light_node.transform = node.matrix_local * AXIS_CORRECT
-        light_node.light_negative = light.use_negative
-        light_node.light_specular = 1.0 if light.use_specular else 0.0
-        light_node.light_energy = light.energy
+        light_node['light_color'] = mathutils.Color(light.color)
+        light_node['transform'] = node.matrix_local * AXIS_CORRECT
+        light_node['light_negative'] = light.use_negative
+        light_node['light_specular'] = 1.0 if light.use_specular else 0.0
+        light_node['light_energy'] = light.energy
 
 
         escn_file.add_node(light_node)
         escn_file.add_node(light_node)
 
 

+ 12 - 5
io_scene_godot/export_godot.py

@@ -101,8 +101,11 @@ class GodotExporter:
 
 
     def export_scene(self):
     def export_scene(self):
         """Decide what objects to export, and export them!"""
         """Decide what objects to export, and export them!"""
-        self.escn_file.add_node(structures.SectionHeading(
-            "node", type="Spatial", name=self.scene.name
+        self.escn_file.add_node(structures.FileEntry(
+            "node", {
+                "type":"Spatial",
+                "name":self.scene.name
+            }
         ))
         ))
         logging.info("Exporting scene: %s", self.scene.name)
         logging.info("Exporting scene: %s", self.scene.name)
 
 
@@ -126,9 +129,13 @@ class GodotExporter:
 
 
     def export(self):
     def export(self):
         """Begin the export"""
         """Begin the export"""
-        self.escn_file = structures.ESCNFile(
-            structures.SectionHeading("gd_scene", load_steps=1, format=2)
-        )
+        self.escn_file = structures.ESCNFile(structures.FileEntry(
+            "gd_scene",
+            {
+                "load_steps":1,
+                "format":2
+            }
+        ))
 
 
         self.export_scene()
         self.export_scene()
         self.escn_file.fix_paths(self.config)
         self.escn_file.fix_paths(self.config)

+ 66 - 108
io_scene_godot/structures.py

@@ -3,6 +3,7 @@
 This file contains classes to help dealing with the actual writing to the file
 This file contains classes to help dealing with the actual writing to the file
 """
 """
 import os
 import os
+import collections
 from .encoders import CONVERSIONS
 from .encoders import CONVERSIONS
 
 
 
 
@@ -47,7 +48,7 @@ class ESCNFile:
 
 
         self.external_resources.append(item)
         self.external_resources.append(item)
         index = len(self.external_resources)
         index = len(self.external_resources)
-        item._heading.id = index
+        item.heading['id'] = index
         self._external_hashes[hashable] = index
         self._external_hashes[hashable] = index
         return index
         return index
 
 
@@ -62,7 +63,7 @@ class ESCNFile:
             raise Exception("Attempting to add object to file twice")
             raise Exception("Attempting to add object to file twice")
         self.internal_resources.append(item)
         self.internal_resources.append(item)
         index = len(self.internal_resources)
         index = len(self.internal_resources)
-        item._heading.id = index
+        item.heading['id'] = index
         self._internal_hashes[hashable] = index
         self._internal_hashes[hashable] = index
         return index
         return index
 
 
@@ -88,152 +89,109 @@ class ESCNFile:
         )
         )
 
 
 
 
-class SectionHeading:
-    """Many things in the escn file are separated by headings. These consist
-    of square brackets with key=value pairs inside them. The first element
-    is not a key-value pair, but describes what type of heading it is.
+class FileEntry(collections.OrderedDict):
+    '''Everything inside the file looks pretty much the same. A heading
+    that looks like [type key=val key=val...] and contents that is newline
+    separated key=val pairs. This FileEntry handles the serialization of
+    on entity into this form'''
+    def __init__(self, entry_type, heading_dict=(), values_dict=()):
+        self.entry_type = entry_type
+        self.heading = collections.OrderedDict(heading_dict)
 
 
-    This class generates a section heading from it's attributes, so you can go:
-    sect = SectionHeading('thingo')
-    sect.foo = "bar"
-    sect.bar = 1234
+        # This string is copied verbaitum, so can be used for custom writing
+        self.contents = ''
 
 
-    and then sect.to_string() will return:
-    [thingo foo=bar bar=1234]
-    """
-    def __init__(self, section_type, **kwargs):
-        self._type = section_type
-        for key in kwargs:
-            self.__dict__[key] = kwargs[key]
-
-    def generate_prop_list(self):
-        """Generate all the key=value pairs into a string from all the
-        attributes in this class"""
+        super().__init__(values_dict)
+
+    def generate_heading_string(self):
+        """Convert the heading dict into [type key=val key=val ...]"""
+        out_str = '[{}'.format(self.entry_type)
+        for var in self.heading:
+            val = self.heading[var]
+
+            if isinstance(val, str):
+                val = '"{}"'.format(val)
+
+            out_str += " {}={}".format(var, val)
+        out_str += ']'
+        return out_str
+
+    def generate_body_string(self):
+        """Convert the contents of the super/internal dict into newline
+        separated key=val pairs"""
         out_str = ''
         out_str = ''
-        attribs = vars(self)
-        for var in attribs:
-            if var.startswith('_'):
-                continue  # Ignore hidden variables
-            val = attribs[var]
+        for var in self:
+            val = self[var]
+
             converter = CONVERSIONS.get(type(val))
             converter = CONVERSIONS.get(type(val))
             if converter is not None:
             if converter is not None:
                 val = converter(val)
                 val = converter(val)
 
 
-            # Extra wrapper for str's
-            if isinstance(val, str):
-                val = '"{}"'.format(val)
-
-            out_str += ' {}={}'.format(var, val)
+            if hasattr(val, "to_string"):
+                val = val.to_string()
 
 
+            out_str += '\n{} = {}'.format(var, val)
         return out_str
         return out_str
 
 
     def to_string(self):
     def to_string(self):
-        """Serializes this heading to a string"""
-        return '\n\n[{} {}]\n'.format(self._type, self.generate_prop_list())
+        """Serialize this entire entry"""
+        return "{}\n{}{}".format(
+            self.generate_heading_string(),
+            self.generate_body_string(),
+            self.contents
+        )
 
 
 
 
-class NodeTemplate:
+class NodeTemplate(FileEntry):
     """Most things inside the escn file are Nodes that make up the scene tree.
     """Most things inside the escn file are Nodes that make up the scene tree.
     This is a template node that can be used to contruct nodes of any type.
     This is a template node that can be used to contruct nodes of any type.
     It is not intended that other classes in the exporter inherit from this,
     It is not intended that other classes in the exporter inherit from this,
-    but rather that all the exported nodes use this template directly.
-
-    Similar to the Sectionheading, this class uses it's attributes to
-    determine the properties of the node."""
+    but rather that all the exported nodes use this template directly."""
     def __init__(self, name, node_type, parent_path):
     def __init__(self, name, node_type, parent_path):
         if parent_path.startswith("./"):
         if parent_path.startswith("./"):
             parent_path = parent_path[2:]
             parent_path = parent_path[2:]
 
 
-        self._heading = SectionHeading(
+        super().__init__(
             "node",
             "node",
-            name=name,
-            type=node_type,
-            parent=parent_path,
-        )
-
-    def generate_prop_list(self):
-        """Generate key/value pairs from the attributes of the node"""
-        out_str = ''
-        attribs = vars(self)
-        for var in attribs:
-            if var.startswith('_'):
-                continue  # Ignore hidden variables
-            val = attribs[var]
-            converter = CONVERSIONS.get(type(val))
-            if converter is not None:
-                val = converter(val)
-            out_str += '\n{} = {}'.format(var, val)
-
-        return out_str
-
-    def to_string(self):
-        """Serialize the node for writing to the file"""
-        return '{}{}\n'.format(
-            self._heading.to_string(),
-            self.generate_prop_list()
+            {
+                "name": name,
+                "type": node_type,
+                "parent": parent_path
+            }
         )
         )
 
 
 
 
-class ExternalResource():
+class ExternalResource(FileEntry):
     """External Resouces are references to external files. In the case of
     """External Resouces are references to external files. In the case of
     an escn export, this is mostly used for images, sounds and so on"""
     an escn export, this is mostly used for images, sounds and so on"""
     def __init__(self, path, resource_type):
     def __init__(self, path, resource_type):
-        self._heading = SectionHeading(
+        super().__init__(
             'ext_resource',
             'ext_resource',
-            id=None,  # This is overwritten by ESCN_File.add_external_resource
-            path=path,
-            type=resource_type
+            {
+                'id':None,  # This is overwritten by ESCN_File.add_external_resource
+                'path':path,
+                'type':resource_type
+            }
         )
         )
 
 
     def fix_path(self, export_settings):
     def fix_path(self, export_settings):
         """Makes the resource path relative to the exported file"""
         """Makes the resource path relative to the exported file"""
-        self._heading.path = os.path.relpath(
-            self._heading.path,
+        self.heading['path'] = os.path.relpath(
+            self.heading['path'],
             os.path.dirname(export_settings["path"]),
             os.path.dirname(export_settings["path"]),
         )
         )
 
 
-    def to_string(self):
-        """Serialize for export"""
-        return self._heading.to_string()
-
 
 
-class InternalResource():
+class InternalResource(FileEntry):
     """ A resource stored internally to the escn file, such as the
     """ A resource stored internally to the escn file, such as the
     description of a material """
     description of a material """
     def __init__(self, resource_type):
     def __init__(self, resource_type):
-        self._heading = SectionHeading(
+        super().__init__(
             'sub_resource',
             'sub_resource',
-            id=None,  # This is overwritten by ESCN_File.add_external_resource
-            type=resource_type
-        )
-
-        # This string is dumped verbatim, so can be used it the key=value
-        # would be hard to manage (Eg meshes, custom array types
-        self.contents = ''
-
-    def generate_prop_list(self):
-        """Generate key/value pairs from the attributes of the node"""
-        out_str = ''
-        attribs = vars(self)
-        for var in attribs:
-            if var.startswith('_') or var == 'contents':
-                continue  # Ignore hidden variables
-            val = attribs[var]
-            converter = CONVERSIONS.get(type(val))
-            if converter is not None:
-                val = converter(val)
-            if hasattr(val, "to_string"):
-                val = val.to_string()
-            out_str += '\n{} = {}'.format(var, val)
-
-        return out_str
-
-    def to_string(self):
-        """Serialize the node for writing to the file"""
-        return '{}{}\n{}'.format(
-            self._heading.to_string(),
-            self.generate_prop_list(),
-            self.contents,
+            {
+                'id':None,  # This is overwritten by ESCN_File.add_external_resource
+                'type':resource_type
+            }
         )
         )