Explorar el Código

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

Geoffrey hace 7 años
padre
commit
cf6bf130cd

+ 7 - 0
Makefile

@@ -1,6 +1,7 @@
 PYLINT = pylint3
 PEP8 = pep8
 BLENDER = blender
+GODOT = godot
 
 pylint:
 	$(PYLINT) io_scene_godot
@@ -14,3 +15,9 @@ pep8:
 test-blends:
 	rm -rf ./tests/.import  # Ensure we don't have any hangover data
 	$(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):
-    """Add to the manu"""
+    """Add to the menu"""
     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_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):
-            mesh_node.transform = node.matrix_local
+            mesh_node['transform'] = node.matrix_local
         else:
-            mesh_node.transform = mathutils.Matrix.Identity(4)
+            mesh_node['transform'] = mathutils.Matrix.Identity(4)
         escn_file.add_node(mesh_node)
 
         return parent_path + '/' + node.name
@@ -295,9 +295,6 @@ class Surface:
         return surface_lines
 
 
-CMP_EPSILON = 0.0001
-
-
 def fix_vertex(vtx):
     """Changes a single position vector from y-up to z-up"""
     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)
 
     if parent_override is None:
-        col_node.transform = mathutils.Matrix.Identity(4) * AXIS_CORRECT
+        col_node['transform'] = mathutils.Matrix.Identity(4) * AXIS_CORRECT
     else:
         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
 
@@ -80,24 +80,24 @@ def export_collision_shape(escn_file, export_settings, node, parent_path,
 
     if rbd.collision_shape == "BOX":
         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)
 
     elif rbd.collision_shape == "SPHERE":
         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)
 
     elif rbd.collision_shape == "CAPSULE":
         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)
     # elif rbd.collision_shape == "CONVEX_HULL":
     #   pass
     elif rbd.collision_shape == "MESH":
         shape_id = generate_triangle_mesh_array(
-            escn_file, export_settings, 
+            escn_file, export_settings,
             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)
 
     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)
 
     return parent_path + "/" + col_name
@@ -140,7 +140,7 @@ def generate_triangle_mesh_array(escn_file, export_settings, node):
 
     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)
 
@@ -163,22 +163,22 @@ def export_physics_controller(escn_file, export_settings, node, parent_path):
     phys_obj = NodeTemplate(phys_name, phys_controller, parent_path)
 
     #  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
     for offset, bit in enumerate(rbd.collision_groups):
         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":
-        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)
 

+ 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']:
         return 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)
 
     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)
     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":
-        cam_node.projection = 0
-        cam_node.fov = math.degrees(camera.angle)
+        cam_node['projection'] = 0
+        cam_node['fov'] = math.degrees(camera.angle)
     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)
 
     return parent_path + '/' + node.name
@@ -60,8 +60,8 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
     if light.type == "POINT":
         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:
             logging.warning(
@@ -70,10 +70,10 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
     elif light.type == "SPOT":
         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:
             logging.warning(
@@ -82,7 +82,7 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
     elif light.type == "SUN":
         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:
         light_node = None
         logging.warning(
@@ -91,11 +91,11 @@ def export_lamp_node(escn_file, export_settings, node, parent_path):
 
     if light_node is not None:
         # 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)
 

+ 12 - 5
io_scene_godot/export_godot.py

@@ -101,8 +101,11 @@ class GodotExporter:
 
     def export_scene(self):
         """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)
 
@@ -126,9 +129,13 @@ class GodotExporter:
 
     def export(self):
         """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.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
 """
 import os
+import collections
 from .encoders import CONVERSIONS
 
 
@@ -47,7 +48,7 @@ class ESCNFile:
 
         self.external_resources.append(item)
         index = len(self.external_resources)
-        item._heading.id = index
+        item.heading['id'] = index
         self._external_hashes[hashable] = index
         return index
 
@@ -62,7 +63,7 @@ class ESCNFile:
             raise Exception("Attempting to add object to file twice")
         self.internal_resources.append(item)
         index = len(self.internal_resources)
-        item._heading.id = index
+        item.heading['id'] = index
         self._internal_hashes[hashable] = 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 = ''
-        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))
             if converter is not None:
                 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
 
     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.
     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,
-    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):
         if parent_path.startswith("./"):
             parent_path = parent_path[2:]
 
-        self._heading = SectionHeading(
+        super().__init__(
             "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
     an escn export, this is mostly used for images, sounds and so on"""
     def __init__(self, path, resource_type):
-        self._heading = SectionHeading(
+        super().__init__(
             '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):
         """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"]),
         )
 
-    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
     description of a material """
     def __init__(self, resource_type):
-        self._heading = SectionHeading(
+        super().__init__(
             '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
+            }
         )