# -*- test-case-name: mv3d.test.phys.test_representation -*- # Copyright (C) 2010-2012 Mortal Coil Games # See LICENSE for details. """ @author: mike """ import os from ConfigParser import ConfigParser from mv3d.util.persist import IDTuple, Persistable, FloatVector, UpgradeError,\ Text from mv3d.net.pb import Cacheable from twisted.spread import pb from mv3d.net.security import Securable try: from xml.etree.cElementTree import SubElement, Element, tostring, fromstring, \ parse except ImportError: from xml.etree.ElementTree import SubElement, Element, tostring, fromstring, \ parse from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, \ gatherResults, DeferredList, maybeDeferred from zope.interface import Interface, implements import mv3d from mv3d.client.ui.irenderer import ISceneNode, IStaticMesh, Ogre3D, Panda3D,\ IGeneratedMesh, LightKind, ILight, IAnimatedMesh, IMaterial from mv3d.util.guide import ChangeNotifier, NotifierProperty, FloatConverter from mv3d.util.math3d import Quaternion, Vector from mv3d.util.iservice import IAssetClient, ISimulationClient from mv3d.phys.iphys import IBoxCollider, ICylinderCollider, ISphereCollider, \ IPlaneCollider, IMeshCollider, ISpace representationFileCache = {} class IPart(Interface): """ Defines any part of a representation. """ def build(conductor, position, rotation, scale, oid=None, #@NoSelf body=None, space=None, renderer=None, parentNode=None): """ Build the part and all sub-parts. Body and space are required for building colliders while renderer and parentNode are required for building visual parts. """ def destroy(): #@NoSelf """ Destroys the part and all sub-parts. """ def showColliders(renderWindow=None, parentNode=None): #@NoSelf """ Displays a visual representation of the colliders on this part and all sub-parts. """ def hideColliders(): #@NoSelf """ Hides the visual representation of the colliders on this part and all sub-parts. """ def showLights(renderWindow=None, parentNode=None): #@NoSelf """ Displays a visual representation of any lights on this part and all sub-parts. """ def hideLights(): #@NoSelf """ Hides the visual representation of any lights on this part and all sub-parts. """ def select(): #@NoSelf """ Visually select this part. """ def deselect(): #@NoSelf """ Deselect the part. """ def save(root): #@NoSelf """ Save this part to the given XML root object. """ def load(root): #@NoSelf """ Load this part from the specified XML root object. """ def getDependencies(): #@NoSelf """ Returns a list of filenames that are dependencies of this part or its sub-parts """ def getRadius(): #@NoSelf """ Returns the radius of the part including all sub-parts """ def getBoundingBox(): #@NoSelf """ Returns the bounding box of the part including all sub-parts """ def getColliders(): #@NoSelf """ Returns all physics colliders for this part and all sub-parts """ def getSceneObjects(): #@NoSelf """ Returns all the scene objects for this part and all sub-parts """ def clone(): #@NoSelf """ Returns a copy of this part """ class IPhysicsPart(IPart): """ Defines a physics related part of a representation """ class IVisualPart(IPart): """ Defines a visual related part of a representation """ def getCompatibleRenderers(): #@NoSelf """ Returns a list of compatible renderers. """ def vectorSetter(attrib, base=""): """ Returns three properties. One for x, y, z which all hit attrib. """ def _getX(self): return getattr(self, attrib)[0] def _setX(self, value): if isinstance(getattr(self, attrib), tuple): setattr(self, attrib, Vector(getattr(self, attrib))) try: getattr(self, attrib)[0] = float(value) except ValueError: pass self.propertyChanged(base + "x") self.propertyChanged(attrib) def _getY(self): return getattr(self, attrib)[1] def _setY(self, value): try: getattr(self, attrib)[1] = float(value) except ValueError: pass self.propertyChanged(base + "y") self.propertyChanged(attrib) def _getZ(self): return getattr(self, attrib)[2] def _setZ(self, value): try: getattr(self, attrib)[2] = float(value) except ValueError: pass self.propertyChanged(base + "z") self.propertyChanged(attrib) return (property(_getX, _setX), property(_getY, _setY), property(_getZ, _setZ)) def parseVector(root): """ Converts an XML tag into a vector """ if root is None: return Vector((0, 0, 0)) return Vector(float(root.attrib["x"]), float(root.attrib["y"]), float(root.attrib["z"])) def makeVector(root, tag, vector): """ Converts a vector into an XML tag """ SubElement(root, tag, dict(x=str(vector[0]), y=str(vector[1]), z=str(vector[2]))) def parseQuaternion(root): """ Converts an XML tag into a quaternion """ if root is None: return Quaternion((1, 0, 0, 0)) return Quaternion(float(root.attrib["w"]), float(root.attrib["x"]), float(root.attrib["y"]), float(root.attrib["z"])) def makeQuaternion(root, tag, quaternion): """ Converts a quaternion into an XML tag """ SubElement(root, tag, dict(w=str(quaternion[0]), x=str(quaternion[1]), y=str(quaternion[2]), z=str(quaternion[3]))) def parseColor(root): """ Converts an XML tag into a color """ if root is None: return Vector((0, 0, 0)) return Vector(float(root.attrib["r"]), float(root.attrib["g"]), float(root.attrib["b"])) def writeRepresentationFile(parts, filename=None): """ Creates an XML representation file and saves it to filename if specified. Returns the raw XML string """ root = Element("Representation", { "xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", "xmlns:tns":"http://www.mv3d.com/representation", "xsi:schemaLocation":"http://www.mv3d.com/representation " "http://www.mv3d.com/trac/export/head/trunk/schema/representation.xsd", }) if filename is not None: representationFileCache[filename] = parts for part in parts: part.save(root) def pretty(element, indent=0, isEnd=False): element.tail = "\n" + " " * (indent - isEnd) if element.text: element.text = "\n" + " " * (indent + 1) + element.text element.text += "\n" + " " * indent elif len(element): element.text = "\n" + " " * (indent + 1) for idx, subelement in enumerate(element): pretty(subelement, indent + 1, idx == len(element) - 1) pretty(root) data = tostring(root) if filename is not None: fil = open(filename, "w") fil.write(data) fil.close() return data def readRepresentationFile(filename=None, xmlString=None, useCache=True): """ Parses a representation XML file or string and returns a list of parts """ if not filename and not xmlString: raise TypeError("One of filename or fromString is required!") cached = representationFileCache.get(filename) if useCache and cached is not None: return [part.clone() for part in cached] if filename: root = parse(filename).getroot() else: root = fromstring(xmlString) parts = Prefab.load(root).parts if useCache and filename: representationFileCache[filename] = parts return parts def toRepresentationFilename(osFilename): """ Converts an os-specific filename to a representation filename """ if osFilename is None: return osFilename return "/".join(osFilename.split(os.path.sep)) def fromRepresentationFilename(repFilename): """ Converts a representation filename to os-specific. """ if repFilename is None: return repFilename return os.path.sep.join(repFilename.split("/")) class Collider(ChangeNotifier): """ Represents a collider """ implements(IPhysicsPart) name = NotifierProperty("name", "Collider") filename = NotifierProperty("filename", None) shape = NotifierProperty("shape", None) position = NotifierProperty("position") rotation = NotifierProperty("rotation") normal = NotifierProperty("normal") x, y, z = vectorSetter("position") rx, ry, rz = vectorSetter("rotation", "r") nx, ny, nz = vectorSetter("normal", "n") radius = NotifierProperty("radius", 1, converter=FloatConverter()) length = NotifierProperty("length", 1, converter=FloatConverter()) width = NotifierProperty("width", 1, converter=FloatConverter()) height = NotifierProperty("height", 1, converter=FloatConverter()) distance = NotifierProperty("distance", 0, converter=FloatConverter()) density = NotifierProperty("density", 1, converter=FloatConverter()) mu = NotifierProperty("mu", 10000, converter=FloatConverter()) mu2 = NotifierProperty("mu2", 0, converter=FloatConverter()) bounce = NotifierProperty("bounce", 0.1, converter=FloatConverter()) bounce_vel = NotifierProperty("bounce_vel", 0.0, converter=FloatConverter()) slip1 = NotifierProperty("slip1", 0.0, converter=FloatConverter()) slip2 = NotifierProperty("slip2", 0.0, converter=FloatConverter()) rollfrict = NotifierProperty("rollfrict", 0.0, converter=FloatConverter()) soft_erp = NotifierProperty("soft_erp", 0.0, converter=FloatConverter()) soft_cfm = NotifierProperty("soft_cfm", 0.0, converter=FloatConverter()) obj = None node = None vertexes = NotifierProperty("vertexes") triangles = NotifierProperty("triangles") collider = None renderer = None space = None materialFile = os.path.abspath(os.path.join(os.path.dirname( mv3d.__file__), "..", "media", "materials", "scripts", "collider.material")) colliderMaterial = None colliderSelectedMaterial = None def __init__(self): ChangeNotifier.__init__(self) self.position = Vector() self.rotation = Vector() self.normal = Vector((0, 1, 0)) self.addPropertyListener("position", self.onMove) self.addPropertyListener("rotation", self.onRotate) self.addPropertyListener("distance", self.onMove) self.addPropertyListener("normal", self.onMove) self.addPropertyListener("radius", self.onSize) self.addPropertyListener("length", self.onSize) self.addPropertyListener("width", self.onSize) self.addPropertyListener("height", self.onSize) def __str__(self): """ Stringify """ return "%s (%s)" % (self.name, self.shape) def clone(self, rename=True): """ Return a copy! """ clone = Collider() for name in Collider.__dict__.keys(): if name == "position": clone.position = Vector(self.position) elif name == "rotation": clone.rotation = Vector(self.rotation) elif name == "scale": clone.scale = Vector(self.scale) elif isinstance(getattr(Collider, name), NotifierProperty): setattr(clone, name, getattr(self, name)) if rename: clone.name = self.name + "Copy" return clone def _rebuild(self): """ If we've been built, rebuild now! """ if self.collider is None: return self.build(self.conductor, self.lastPosition, self.lastRotation, self.lastScale, self.oid, self.body, self.space) def _loadMaterial(self): """ Load and parse a material file """ if self.renderer is None: self.colliderMaterial = "ColliderMaterial" self.colliderSelectedMaterial = "ColliderSelectedMaterial" return try: self.colliderMaterial = self.renderer.getRendererClass( IMaterial).load(self.renderer, self.materialFile, "ColliderMaterial") self.colliderSelectedMaterial = self.renderer.getRendererClass( IMaterial).load(self.renderer, self.materialFile, "ColliderSelectedMaterial") except AttributeError: # load isn't supported on all renderers yet. self.colliderMaterial = self.renderer.getRendererClass( IMaterial)(self.renderer, "ColliderMaterial") self.colliderMaterial.setColor((1, 1, 1, 1)) self.colliderSelectedMaterial = self.renderer.getRendererClass( IMaterial)(self.renderer, "ColliderSelectedMaterial") self.colliderSelectedMaterial.setColor((1, 1, 0.2, 1.5)) def onMove(self, _obj, _prop): """ We've been moved! """ self._rebuild() if self.node is None: return if self.shape == "Plane": self.node.setPosition(self.normal * self.distance) self.node.setOrientation(Quaternion.fromTargetPosition(self.normal)) else: self.node.setPosition((self.x, self.y, self.z)) def onRotate(self, _obj, _prop): """ We've been rotated """ self._rebuild() if self.node is not None and self.shape != "Plane": self.node.setOrientation(Quaternion.fromEuler((self.rx, self.ry, self.rz))) def onSize(self, _obj, _prop): """ We've been sized! """ self._rebuild() if self.node is None: return if self.shape == "Sphere": self.node.setScale((self.radius * 2, self.radius * 2, self.radius * 2)) elif self.shape == "Box": self.node.setScale((self.width, self.height, self.length)) elif self.shape == "Cylinder": self.node.setScale((self.radius * 2, self.length, self.radius * 2)) def showColliders(self, renderWindow=None, parentNode=None, planeScale=30): """ Figure out the shape and make a preview object for it. """ assert not (renderWindow is None and parentNode is None) if renderWindow is None: renderer = parentNode.renderer else: renderer = renderWindow.renderer self.renderer = renderer self._loadMaterial() meshName = None if self.shape[0].islower(): self.shape.capitalize() if self.shape == "Sphere": meshName = "unit-sphere" elif self.shape == "Box": meshName = "unit-cube" elif self.shape == "Cylinder": meshName = "unit-cylinder" elif self.shape == "Plane": meshName = "unit-plane" if meshName is not None: meshName = os.path.join(os.path.dirname( mv3d.__file__), "..", "media", "models", meshName + "." + renderer.getMeshFileExtension()) obj = renderer.createFromFile(str(id(self)), meshName) elif self.shape == "TriangleMesh": obj = renderer.getRendererClass(IGeneratedMesh)(renderer) obj.createManual(str(id(self)), self.colliderMaterial, self.vertexes, self.triangles) if parentNode is None: parent = renderWindow.sceneNode else: parent = parentNode node = renderer.getRendererClass(ISceneNode)(renderer, parent) node.initialize(str(id(self))) if self.shape == "Plane": node.setScale((planeScale, planeScale, planeScale)) node.addObject(obj) if renderWindow is not None: renderWindow.items.append(obj.getObject()) self.obj = obj self.node = node obj.setMaterial(self.colliderMaterial) self.renderer = renderer self.onMove(None, None) self.onRotate(None, None) self.onSize(None, None) def hideColliders(self): """ Hide the colliders from view """ if self.node is not None: self.node.finalize(True) self.node = None self.obj = None def showLights(self, renderWindow=None, parentNode=None): """ Displays a visual representation of any lights on this part and all sub-parts. """ def hideLights(self): """ Hides the visual representation of any lights on this part and all sub-parts. """ def destroy(self, renderWindow=None): """ Destroy the collider """ if renderWindow is not None: renderWindow.items.remove(self.obj.getObject()) if self.node is not None: self.node.finalize(True) self.obj = None self.node = None def select(self): """ Visually show that this is selected """ if self.renderer is None: return self.obj.setMaterial(self.colliderSelectedMaterial) def deselect(self): """ Return back to deselected state """ if self.renderer is None: return self.obj.setMaterial(self.colliderMaterial) def save(self, root): """ Save this collider at the root. """ shape = self.shape collider = SubElement(root, shape, dict(name=self.name)) if self.shape != "Plane": SubElement(collider, "position", dict(x=str(self.x), y=str(self.y), z=str(self.z))) SubElement(collider, "rotation", dict(x=str(self.rx), y=str(self.ry), z=str(self.rz))) collider.attrib["density"] = str(self.density) if self.filename is not None: collider.attrib["filename"] = toRepresentationFilename( self.filename) return collider.attrib["mu"] = str(self.mu) collider.attrib["mu2"] = str(self.mu2) collider.attrib["bounce"] = str(self.bounce) collider.attrib["bounce_vel"] = str(self.bounce_vel) collider.attrib["slip1"] = str(self.slip1) collider.attrib["slip2"] = str(self.slip2) collider.attrib["rollfrict"] = str(self.rollfrict) collider.attrib["soft_erp"] = str(self.soft_erp) collider.attrib["soft_cfm"] = str(self.soft_cfm) if shape == "Box": collider.attrib["length"] = str(self.length) collider.attrib["width"] = str(self.width) collider.attrib["height"] = str(self.height) elif shape == "Cylinder": collider.attrib["radius"] = str(self.radius) collider.attrib["length"] = str(self.length) elif shape == "Sphere": collider.attrib["radius"] = str(self.radius) elif shape == "Plane": collider.attrib["distance"] = str(self.distance) SubElement(collider, "normal", dict(x=str(self.nx), y=str(self.ny), z=str(self.nz))) elif shape == "TriangleMesh": mesh = SubElement(collider, "mesh") for vertex in self.vertexes: makeVector(mesh, "vertex", vertex) for triangle in self.triangles: tri = SubElement(mesh, "triangle") for vert in triangle: SubElement(tri, "vertex").text = str(vert) @classmethod def load(cls, root, name=None): """ Load the collider from XML. """ instance = cls() if name is not None: return instance.loadOld(root, name) instance.name = root.attrib["name"] instance.shape = root.tag if instance.shape != "Plane": instance.position = parseVector(root.find("position")) instance.rotation = parseVector(root.find("rotation")) instance.density = float(root.attrib["density"]) instance.mu = float(root.attrib["mu"]) instance.mu2 = float(root.attrib["mu2"]) instance.bounce = float(root.attrib["bounce"]) instance.bounce_vel = float(root.attrib["bounce_vel"]) instance.slip1 = float(root.attrib["slip1"]) instance.slip2 = float(root.attrib["slip2"]) instance.rollfrict = float(root.attrib["rollfrict"]) instance.soft_erp = float(root.attrib["soft_erp"]) instance.soft_cfm = float(root.attrib["soft_cfm"]) if instance.shape == "Box": instance.length = float(root.attrib["length"]) instance.width = float(root.attrib["width"]) instance.height = float(root.attrib["height"]) elif instance.shape == "Cylinder": instance.radius = float(root.attrib["radius"]) instance.length = float(root.attrib["length"]) elif instance.shape == "Sphere": instance.radius = float(root.attrib["radius"]) elif instance.shape == "Plane": instance.distance = float(root.attrib["distance"]) instance.normal = parseVector(root.find("normal")) elif instance.shape == "TriangleMesh": mesh = root.find("mesh") instance.vertexes = [parseVector(vert) for vert in mesh.findall("vertex")] instance.triangles = [] for triangle in mesh.findall("triangle"): instance.triangles.append([int(vert.text) for vert in triangle.findall("vertex")]) return instance def loadOld(self, cfg, nm): """ Load the collider """ self.name = nm self.shape = cfg.get(self.name, "shape").capitalize() self.x = cfg.getfloat(self.name, "x") self.y = cfg.getfloat(self.name, "y") self.z = cfg.getfloat(self.name, "z") self.rx = cfg.getfloat(self.name, "rx") self.ry = cfg.getfloat(self.name, "ry") self.rz = cfg.getfloat(self.name, "rz") if self.shape == "Box": self.length = cfg.getfloat(self.name, "length") self.width = cfg.getfloat(self.name, "width") self.height = cfg.getfloat(self.name, "height") elif self.shape == "Cylinder": self.length = cfg.getfloat(self.name, "length") self.radius = cfg.getfloat(self.name, "radius") elif self.shape == "Sphere": self.radius = cfg.getfloat(self.name, "radius") elif self.shape == "Plane": self.distance = cfg.getfloat(self.name, "distance") self.density = cfg.getfloat(self.name, "density") self.mu = cfg.getfloat(self.name, "mu") self.mu2 = cfg.getfloat(self.name, "mu2") self.bounce = cfg.getfloat(self.name, "bounce") self.bounce_vel = cfg.getfloat(self.name, "bounce_vel") self.slip1 = cfg.getfloat(self.name, "slip1") self.slip2 = cfg.getfloat(self.name, "slip2") self.rollfrict = cfg.getfloat(self.name, "rollfrict") self.soft_erp = cfg.getfloat(self.name, "soft_erp") self.soft_cfm = cfg.getfloat(self.name, "soft_cfm") return self def build(self, conductor, position, rotation, scale, oid=None, #@UnusedVariable body=None, space=None, renderer=None, parentNode=None, #@UnusedVariable colliderArgs=None): """ Actually build this collider. """ ctype = None scale = Vector(scale) colliderArgs = colliderArgs or {} if self.collider is not None: self.collider.destroy() args = dict( position=Vector(position) + Quaternion(rotation) * (scale[0] * self.x, scale[1] * self.y, scale[2] * self.z), rotation=Quaternion(rotation) * Quaternion.fromEuler(self.rx, self.ry, self.rz), density=self.density, mu=self.mu, mu2=self.mu2, bounce=self.bounce, bounce_vel=self.bounce_vel, slip1=self.slip1, slip2=self.slip2, rollfrict=self.rollfrict, soft_erp=self.soft_erp, soft_cfm=self.soft_cfm ) physEngine = None if space is not None and hasattr(space, "world"): physEngine = space.world elif body is not None and hasattr(body, "world"): physEngine = body.world elif oid is not None: sim = conductor.getLocalService(ISimulationClient) physEngine = sim.getLocalRealm(oid[0]).world assert physEngine is not None if self.shape == "Box": ctype = physEngine.getPhysicsClass(IBoxCollider) args["size"] = (scale[0] * self.width, scale[1] * self.height, scale[2] * self.length) elif self.shape == "Cylinder": ctype = physEngine.getPhysicsClass(ICylinderCollider) args["length"] = scale[1] * self.length args["radius"] = scale[0] * self.radius elif self.shape == "Sphere": ctype = physEngine.getPhysicsClass(ISphereCollider) args["radius"] = scale[1] * self.radius elif self.shape == "Plane": ctype = physEngine.getPhysicsClass(IPlaneCollider) args["normal"] = args["rotation"].rotate(Vector(self.normal)) args["distance"] = self.distance * Vector(scale).length() + ( Quaternion.fromTargetPosition( args["normal"]).rotate(args["position"])).length() elif self.shape == "TriangleMesh": ctype = physEngine.getPhysicsClass(IMeshCollider) args["verts"] = [(vert[0] * scale[0], vert[1] * scale[1], vert[2] * scale[2]) for vert in self.vertexes] args["tris"] = self.triangles else: raise TypeError("Unsupported collider type %s" % self.shape) args["oid"] = oid args.update(colliderArgs) coll = ctype(**args) coll.build(space=space, body=body) self.collider = coll self.space = space self.lastPosition = position self.lastRotation = rotation self.lastScale = scale self.body = body self.oid = oid self.conductor = conductor return coll def getDependencies(self): """ Returns a list of filenames that are dependencies of this part or its sub-parts """ if self.filename is not None: return [self.filename] return [] def getRadius(self): """ Returns the radius of the part including all sub-parts """ bounds = self.getBoundingBox() size = Vector(bounds[1][0] - bounds[0][0], bounds[1][1] - bounds[0][1], bounds[1][2] - bounds[0][2]) return size.length() / 2.0 def getBoundingBox(self): """ Returns the bounding box of the part including all sub-parts """ bounds = self.collider.getBoundingBox() return ((bounds[0], bounds[2], bounds[4]), (bounds[1], bounds[3], bounds[5])) def getColliders(self): """ Returns the colliders for this part """ return [self.collider] def getSceneObjects(self): """ Colliders don't have scene objects, so returns nothing """ return [] def isSameObject(self, _entity): """ Returns true if this prefab has the entity in question. """ return False class Object(ChangeNotifier): """ An object encompases visual and physical components. ** DEPRECATED ** Please use Prefab / Collider / Mesh. """ name = NotifierProperty("name", None) asset = NotifierProperty("asset", None) position = NotifierProperty("position", None) rotation = NotifierProperty("rotation", None) scale = NotifierProperty("scale", None) x, y, z = vectorSetter("position") rx, ry, rz = vectorSetter("rotation", "r") sx, sy, sz = vectorSetter("scale", "s") visualAssetID = None obj = None node = None selectBox = None colliderNode = None def __init__(self, asset=None): ChangeNotifier.__init__(self) self.position = (0, 0, 0) self.rotation = (0, 0, 0) self.scale = (1, 1, 1) self.addPropertyListener("position", self.onMove) self.addPropertyListener("rotation", self.onRotate) self.addPropertyListener("scale", self.onSize) self.asset = asset self.colliders = [] if self.asset is not None: self.loadVisualAssetID() def __str__(self): """ Stringify """ return str(self.name) def build(self, conductor, position, rotation, scale, oid=None, body=None, space=None, renderer=None, parentNode=None, colliderArgs=None): """ Actually build this object. """ self.buildColliders(space, position, rotation, scale, oid, body, colliderArgs) return self.buildObjects(conductor, renderer, parentNode) def loadVisualAssetID(self): """ Load the visual asset id off of the solid file """ cfg = ConfigParser() cfg.read(self.asset.getFullFile()) asset = cfg.get("Solidifier", "modelAsset") self.visualAssetID = tuple([int(aid.strip()) for aid in asset.split(",")]) def getCompatibleRenderers(self, conductor): """ Returns a list of compatible renderers. """ return Ogre3D, Panda3D def clone(self): """ Return a duplicate copy """ copy = Object(self.asset) for name in Object.__dict__.keys(): if isinstance(getattr(Object, name), NotifierProperty): setattr(copy, name, getattr(self, name)) return copy def save(self, cfg): """ Save the object """ cfg.add_section(self.name) cfg.set(self.name, "asset", ", ".join([str(aid) for aid in self.asset.aid])) cfg.set(self.name, "x", str(self.x)) cfg.set(self.name, "y", str(self.y)) cfg.set(self.name, "z", str(self.z)) cfg.set(self.name, "rx", str(self.rx)) cfg.set(self.name, "ry", str(self.ry)) cfg.set(self.name, "rz", str(self.rz)) cfg.set(self.name, "sx", str(self.sx)) cfg.set(self.name, "sy", str(self.sy)) cfg.set(self.name, "sz", str(self.sz)) @classmethod @inlineCallbacks def loadFromFile(cls, filename, name, asvc): """ Load directly from a file """ obj = Object() cfg = ConfigParser() cfg.read(filename) yield obj.load(cfg, name, asvc) returnValue(obj) @inlineCallbacks def load(self, cfg, name, asvc): """ Load the object out of a builder file """ self.name = name aid = tuple([int(itm.strip()) for itm in cfg.get(self.name, "asset").split(",")]) asset = yield asvc.acquireAsset(aid) self.x = float(cfg.get(self.name, "x")) self.y = float(cfg.get(self.name, "y")) self.z = float(cfg.get(self.name, "z")) self.rx = float(cfg.get(self.name, "rx")) self.ry = float(cfg.get(self.name, "ry")) self.rz = float(cfg.get(self.name, "rz")) self.sx = float(cfg.get(self.name, "sx")) self.sy = float(cfg.get(self.name, "sy")) self.sz = float(cfg.get(self.name, "sz")) self.loadFromAsset(asset) def loadFromAsset(self, asset): """ Load the object from an asset that's been acquired. """ self.asset = asset cfg = ConfigParser() cfg.read(self.asset.getFullFile()) for section in cfg.sections(): if section == "Solidifier": if cfg.has_option(section, "modelAsset"): aid = tuple([int(comp.strip()) for comp in cfg.get(section, "modelAsset").split(",")]) self.visualAssetID = aid else: self.colliders.append(Collider.load(cfg, section)) @inlineCallbacks def createObject(self, conductor, renderWindow, parentNode=None): """ Build the object in the render window """ if self.visualAssetID is None: self.loadVisualAssetID() asvc = conductor.getLocalService(IAssetClient) visAsset = yield asvc.acquireAsset(self.visualAssetID) if self.node is not None: self.node.finalize(True) obj = yield renderWindow.renderer.createFromAsset(id(self), visAsset) if parentNode is None: parentNode = renderWindow.sceneNode node = renderWindow.renderer.getRendererClass(ISceneNode)( renderWindow.renderer, parentNode) node.initialize(str(id(self))) node.addObject(obj) renderWindow.items.append(obj.getObject()) self.obj = obj self.node = node self.onMove(None, None) self.onRotate(None, None) self.onSize(None, None) size = obj.getRadius() renderWindow.orbitDist = size * 2 + 5 def destroyObject(self, renderWindow): """ Remove the object from the renderWindow """ renderWindow.items.remove(self.obj.object) self.node.finalize(True) self.obj = None self.node = None def destroy(self): """ Remove the object """ if self.node is not None: self.node.finalize(True) self.obj = None self.node = None def onMove(self, _obj, _prop): """ We've been moved! """ if self.node is not None: self.node.setPosition((self.x, self.y, self.z)) def onRotate(self, _obj, _prop): """ We've been rotated """ if self.node is not None: self.node.setOrientation(Quaternion.fromEuler((self.rx, self.ry, self.rz))) def onSize(self, _obj, _prop): """ We've been sized! """ if self.node is not None: self.node.setScale((self.sx, self.sy, self.sz)) def select(self): """ The object was selected. """ if self.node is None: return self.obj.select(self.node) def deselect(self): """ The object was deselected """ if self.node is None or self.selectBox is None: return self.obj.deselect(self.node) def buildColliders(self, space, position, rotation, scale, oid=None, body=None, colliderArgs=None): """ Build the colliders for this object """ if scale is None: scale = [1, 1, 1] pos = Vector(position) + Quaternion(rotation) * (self.x * scale[0], self.y * scale[1], self.z * scale[2]) rot = Quaternion(rotation) * Quaternion.fromEuler(self.rx, self.ry, self.rz) scl = (scale[0] * self.sx, scale[1] * self.sy, scale[2] * self.sz) colls = [] for collider in self.colliders: colls.append(collider.build(None, pos, rot, scl, oid, body, space, colliderArgs=colliderArgs)) return colls @inlineCallbacks def buildPaged(self, conductor, paged, position, rotation, scale): """ Build the object as a paged geometry NOTE: only supports y rotation and scale only works if it is uniform """ if scale is None: scale = [1, 1, 1] pos = Vector(position) + Quaternion(rotation) * (self.x * scale[0], self.y * scale[1], self.z * scale[2]) rot = rotation + self.ry scl = scale * self.sy if self.visualAssetID is None: self.loadVisualAssetID() asvc = conductor.getLocalService(IAssetClient) visAsset = yield asvc.acquireAsset(self.visualAssetID) assert scl >= 0.0 and scl <= 2.0, "Paged scale out of bounds" paged.addGeometry(visAsset, pos, rot, scl) @inlineCallbacks def buildObjects(self, conductor, renderer, parentNode): """ Build objects for this. """ if renderer is None: return if self.visualAssetID is None: self.loadVisualAssetID() asvc = conductor.getLocalService(IAssetClient) visAsset = yield asvc.acquireAsset(self.visualAssetID) if self.node is not None: self.node.finalize(True) obj = yield renderer.createFromAsset(id(self), visAsset) node = renderer.getRendererClass(ISceneNode)(renderer, parentNode.node) node.initialize(str(id(self))) node.addObject(obj) self.obj = obj self.node = node self.onMove(None, None) self.onRotate(None, None) self.onSize(None, None) returnValue(self.obj) def destroyObjects(self): """ Destroy the objects """ if self.node is not None: self.node.finalize(True) def showColliders(self, renderWindow=None, parentNode=None): #@UnusedVariable """ Creates visible objects for the colliders """ if self.colliderNode is not None or self.node is None: return self.colliderNode = self.node.renderer.getRendererClass(ISceneNode)( self.node.renderer, self.node.node) self.colliderNode.initialize("colliderNode%d" % id(self)) for collider in self.colliders: collider.showColliders(None, self.colliderNode) def hideColliders(self): """ Hides all the visible colliders """ if self.colliderNode is not None: self.colliderNode.finalize(True) self.colliderNode = None for collider in self.colliders: collider.destroy() def showLights(self, renderWindow=None, parentNode=None): """ Displays a visual representation of any lights on this part and all sub-parts. """ def hideLights(self): """ Hides the visual representation of any lights on this part and all sub-parts. """ def getColliders(self): """ Returns all the colliders for this object. """ return self.colliders def getSceneObjects(self): """ Returns all scene objects for this object """ return [self.obj] def isSameObject(self, entity): """ Returns true if this prefab has the entity in question. """ return self.obj.isSameObject(entity) def getRadius(self): """ Returns the radius of the part including all sub-parts """ return self.obj.getRadius() class Mesh(ChangeNotifier): """ This is a mesh part type for prefabs. """ implements(IVisualPart, IAnimatedMesh) name = NotifierProperty("name", None) filename = NotifierProperty("filename", None) position = NotifierProperty("position", (0, 0, 0)) rotation = NotifierProperty("rotation", (0, 0, 0)) scale = NotifierProperty("scale", (1, 1, 1)) x, y, z = vectorSetter("position") rx, ry, rz = vectorSetter("rotation", "r") sx, sy, sz = vectorSetter("scale", "s") skeleton = NotifierProperty("skeleton", None) animations = NotifierProperty("animations", None) isAnimated = NotifierProperty("isAnimated", False) node = None obj = None selectBox = None def __init__(self): self.position = Vector() self.rotation = Vector() self.scale = Vector((1, 1, 1)) self.animations = [] self.addPropertyListener("position", self.onMove) self.addPropertyListener("rotation", self.onRotate) self.addPropertyListener("scale", self.onSize) def __str__(self): return self.name or "Mesh" def build(self, conductor, position, rotation, scale, oid=None, #@UnusedVariable body=None, space=None, renderer=None, parentNode=None, #@UnusedVariable colliderArgs=None): #@UnusedVariable """ Build the part and all sub-parts. Body and space are required for building colliders while renderer and parentNode are required for building visual parts. """ if renderer is None: return if self.node is not None: self.node.finalize(True) if parentNode is None: parentNode = renderer.rootNode classType = IAnimatedMesh if self.isAnimated else IStaticMesh obj = renderer.getRendererClass(classType)(renderer, str(id(self))) obj.create(os.path.join(conductor.dataRoot, self.filename), "%s%s" % (id(self), self.name)) if self.skeleton is not None: obj.loadSkeleton(os.path.join(conductor.dataRoot, self.skeleton)) for animation in self.animations: obj.loadAnimations(os.path.join(conductor.dataRoot, animation)) node = renderer.getRendererClass(ISceneNode)(renderer, parentNode) node.initialize(str(id(self))) node.addObject(obj) self.obj = obj self.node = node self.onMove(None, None) self.onRotate(None, None) self.onSize(None, None) def destroy(self): """ Destroys the part and all sub-parts. """ if self.node is not None: self.node.finalize(True) self.node = None def clone(self): """ Returns a copy of this part. """ cloned = Mesh() cloned.position = Vector(self.position) cloned.rotation = Vector(self.rotation) cloned.scale = Vector(self.scale) cloned.name = self.name cloned.filename = self.filename return cloned def showColliders(self, renderWindow=None, parentNode=None): """ Displays a visual representation of the colliders on this part and all sub-parts. """ def hideColliders(self): """ Hides the visual representation of the colliders on this part and all sub-parts. """ def showLights(self, renderWindow=None, parentNode=None): """ Displays a visual representation of any lights on this part and all sub-parts. """ def hideLights(self): """ Hides the visual representation of any lights on this part and all sub-parts. """ def onMove(self, _obj, _prop): """ We've been moved! """ if self.node is not None: self.node.setPosition(self.position) def onRotate(self, _obj, _prop): """ We've been rotated """ if self.node is not None: self.node.setOrientation(Quaternion.fromEuler(self.rotation)) def onSize(self, _obj, _prop): """ We've been sized! """ if self.node is not None: self.node.setScale(self.scale) def setPosition(self, position): """ Currently only works for situations where the colliders are attached to a body. """ if self.node is not None: self.node.setPosition(position) def setOrientation(self, orientation): """ Currently only works for situations where the colliders are attached to a body. """ if self.node is not None: self.node.setOrientation(orientation) def select(self): """ Visually select this part. """ if self.node is None: return self.obj.select(self.node) def deselect(self): """ Deselect the part. """ if self.node is None or self.selectBox is None: return self.obj.deselect(self.node) def save(self, root): """ Save this part to the given XML root object. """ mesh = SubElement(root, "Mesh", dict(name=self.name or "", filename=toRepresentationFilename(self.filename)), isAnimated=str(self.isAnimated)) if self.skeleton is not None: mesh.attrib["skeleton"] = self.skeleton makeVector(mesh, "position", self.position) makeVector(mesh, "rotation", self.rotation) makeVector(mesh, "scale", self.scale) for anim in self.animations: SubElement(mesh, "animation").text = anim return mesh @classmethod def load(cls, root): """ Load this part from the specified XML root object. """ mesh = Mesh() mesh.name = root.attrib["name"] mesh.isAnimated = root.attrib.get("isAnimated", "").lower() == "true" mesh.skeleton = root.attrib.get("skeleton") mesh.filename = fromRepresentationFilename(root.attrib["filename"]) mesh.position = parseVector(root.find("position")) mesh.rotation = parseVector(root.find("rotation")) mesh.scale = parseVector(root.find("scale")) if mesh.scale is None: mesh.scale = Vector((1, 1, 1)) animationNodes = root.findall("animation") or [] mesh.animations.extend([node.text.strip() for node in animationNodes]) return mesh def getDependencies(self): """ Returns a list of filenames that are dependencies of this part or its sub-parts """ if self.filename is not None: return [self.filename] return [] def getRadius(self): """ Returns the radius of the part including all sub-parts """ return self.obj.getRadius() def getBoundingBox(self): """ Returns the bounding box of the part including all sub-parts """ return self.obj.getBoundingBox() def getColliders(self): """ There aren't any colliders for a mesh """ return [] def getSceneObjects(self): """ Returns all scene objects for this object """ return [self.obj] def isSameObject(self, entity): """ Returns true if this prefab has the entity in question. """ return self.obj.isSameObject(entity) def getAnimations(self): """ Return a list of the animation names """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getAnimations() return [] def getActiveAnimations(self): """ Returns a list of the animations that are currently active. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getActiveAnimations() return [] def getAnimationLength(self, animationName): """ Returns the length in seconds of the specified animation. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getAnimationLength(animationName) return 0.0 def getAnimationPosition(self, animationName): """ Returns the current position in animation time for the animation. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getAnimationPosition(animationName) return 0.0 def getAnimationSpeed(self, animationName): """ Returns the speed at which an animation is playing. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getAnimationSpeed(animationName) return 0.0 def modifyActiveAnimation(self, name, position=None, loop=None, weight=None, speed=None): """ Modify properties of an active animation. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.modifyActiveAnimation(name, position, loop, weight, speed) return 0.0 def getBones(self): """ Returns a list of bones in the skeleton. """ if IAnimatedMesh.providedBy(self.obj): return self.obj.getBones() return [] def createSocket(self, name, boneName, position, orientation): """ Creates a new named socket with the given position and orientation relative to the specified bone. """ if IAnimatedMesh.providedBy(self.obj): self.obj.createSocket(name, boneName, position, orientation) def attachToSocket(self, socketName, sceneObject): """ Attach a scene object to a socket. """ if IAnimatedMesh.providedBy(self.obj): self.obj.attachToSocket(socketName, sceneObject) def detachFromSocket(self, socketName, sceneObject): """ Detach a previously attached scene object from a socket. """ if IAnimatedMesh.providedBy(self.obj): self.obj.detachFromSocket(socketName, sceneObject) def startAnimation(self, name, loop=True, weight=100, speed=1.0): """ Start the named animation optionally looping and with the specified weight. """ if IAnimatedMesh.providedBy(self.obj): self.obj.startAnimation(name, loop, weight, speed) def stopAnimation(self, name): """ Stop a currently playing animation """ if IAnimatedMesh.providedBy(self.obj): self.obj.stopAnimation(name) class Light(ChangeNotifier): """ A light that is part of a representation. """ implements(IPart) name = NotifierProperty("name", "") kind = NotifierProperty("kind", "point") filename = NotifierProperty("filename", None) position = NotifierProperty("position", (0, 0, 0)) direction = NotifierProperty("direction", (0, 0, 0)) diffuse = NotifierProperty("diffuse", (1, 1, 1)) specular = NotifierProperty("specular", (1, 1, 1)) x, y, z = vectorSetter("position") rx, ry, rz = vectorSetter("direction", "r") dx, dy, dz = vectorSetter("diffuse", "d") sx, sy, sz = vectorSetter("specular", "s") inner = NotifierProperty("inner", 0.0) outer = NotifierProperty("outer", 0.0) falloff = NotifierProperty("falloff", 0.0) shadows = NotifierProperty("shadows", False) node = None obj = None lastPosition = None lastRotation = None lastScale = None renderer = None parentNode = None helperObj = None helperNode = None colliderMaterial = None colliderSelectedMaterial = None def __init__(self): self.position = Vector() self.direction = Vector() self.diffuse = Vector() self.specular = Vector() self.listen("position", self.onMove) self.listen("direction", self.onRotate) self.listen("diffuse", self.onUpdateProperties) self.listen("specular", self.onUpdateProperties) self.listen("inner", self.onUpdateProperties) self.listen("outer", self.onUpdateProperties) self.listen("falloff", self.onUpdateProperties) def __str__(self): return self.name or self.kind or "Light" def build(self, conductor, position, rotation, scale, oid=None, body=None, space=None, renderer=None, parentNode=None, colliderArgs=None): """ Create the light """ if self.node is not None and (renderer is not None or parentNode is not None): self.node.finalize(True) self.node = None if renderer is not None or parentNode is not None: if parentNode is None: parentNode = renderer.rootNode elif renderer is None: renderer = parentNode.renderer if self.node is None: self.node = renderer.getRendererClass(ISceneNode)(renderer, parentNode) self.node.initialize(str(id(self)) + self.name) if self.position is not None: self.node.setPosition(self.position) if renderer is not None: obj = renderer.getRendererClass(ILight)(renderer, str(id(self)), getattr(LightKind, self.kind)) if self.direction is not None: direction = Quaternion.fromEuler(self.direction) else: direction = None obj.create(self.diffuse, self.specular, None, direction, self.inner, self.outer, self.falloff) self.node.addObject(obj) self.obj = obj self.lastPosition = position self.lastRotation = rotation self.lastScale = scale self.renderer = renderer self.parentNode = parentNode def _loadMaterial(self): """ Load and parse a material file """ if self.renderer is None: self.colliderMaterial = "ColliderMaterial" self.colliderSelectedMaterial = "ColliderSelectedMaterial" return try: self.colliderMaterial = self.renderer.getRendererClass( IMaterial).load(self.renderer, self.materialFile, "ColliderMaterial") self.colliderSelectedMaterial = self.renderer.getRendererClass( IMaterial).load(self.renderer, self.materialFile, "ColliderSelectedMaterial") except AttributeError: # load isn't supported on all renderers yet. self.colliderMaterial = self.renderer.getRendererClass( IMaterial)(self.renderer, "ColliderMaterial") self.colliderMaterial.setColor((1, 1, 1, 1)) self.colliderMaterial = self.renderer.getRendererClass( IMaterial)(self.renderer, "ColliderSelectedMaterial") self.colliderMaterial.setColor((1, 1, 0.2, 1.5)) def destroy(self): """ Destroy the light """ if self.node is not None: self.node.finalize(True) self.node = None self.obj = None def clone(self): """ Return a clone of this light """ cloned = Light() cloned.name = self.name cloned.inner = self.inner cloned.outer = self.outer cloned.falloff = self.falloff cloned.shadows = self.shadows cloned.filename = self.filename cloned.position = Vector(self.position) cloned.direction = Vector(self.direction) cloned.diffuse = Vector(self.diffuse) cloned.specular = Vector(self.specular) return cloned def save(self, root): """ Save this part to the given XML root object. """ light = SubElement(root, self.kind.capitalize() + "Light", dict( name=self.name, inner=str(self.inner or 0.0), outer=str(self.outer or 0.0), falloff=str(self.falloff or 0.0))) if self.filename is not None: light.attrib["filename"] = toRepresentationFilename(self.filename) makeVector(light, "position", self.position) makeVector(light, "direction", self.direction) makeVector(light, "diffuse", self.diffuse) makeVector(light, "specular", self.specular) return light @classmethod def load(cls, root): """ Load this part from the specified XML root object. """ light = Light() light.name = root.attrib["name"] light.kind = root.tag.lower().replace("light", "") light.filename = fromRepresentationFilename(root.attrib.get("filename", None)) light.position = parseVector(root.find("position")) if light.filename is not None: raise RuntimeError("Loading from filename is not implemented.") light.direction = parseVector(root.find("direction")) light.diffuse = parseVector(root.find("diffuse")) light.specular = parseVector(root.find("specular")) light.inner = float(root.attrib.get("inner", 0)) light.outer = float(root.attrib.get("outer", 0)) light.falloff = float(root.attrib.get("falloff", 0)) light.shadows = bool(root.attrib.get("shadows", "").lower() == "true") return light def showColliders(self, renderWindow=None, parentNode=None): """ Displays a visual representation of the colliders on this part and all sub-parts. """ def hideColliders(self): """ Hides the visual representation of the colliders on this part and all sub-parts. """ def showLights(self, renderWindow=None, parentNode=None): """ Displays a visual representation of any lights on this part and all sub-parts. """ assert not (renderWindow is None and parentNode is None) self._loadMaterial() if self.helperNode is not None: self.helperNode.finalize(True) self.helperNode = None self.helperObj = None if renderWindow is None: renderer = parentNode.renderer else: renderer = renderWindow.renderer meshName = None if self.kind == "directional": meshName = "spotlight" elif self.kind == "point": meshName = "pointlight" elif self.kind == "spot": meshName = "spotlight" elif self.kind == "ambient": meshName = "pointlight" else: raise ValueError("Invalid light type %s" % self.kind) if meshName is not None: meshName = os.path.join(os.path.dirname( mv3d.__file__), "..", "media", "models", meshName + "." + renderer.getMeshFileExtension()) obj = renderer.createFromFile(str(id(self)), meshName) if self.kind == "directional": parent = renderWindow.sceneNode else: parent = self.node node = renderer.getRendererClass(ISceneNode)(renderer, parent) node.initialize(str(id(self))) node.addObject(obj) self.helperNode = node if renderWindow is not None: renderWindow.items.append(obj.getObject()) self.helperObj = obj obj.setMaterial(self.colliderMaterial) self.renderer = renderer self.onMove(None, None) self.onRotate(None, None) def hideLights(self): """ Hides the visual representation of any lights on this part and all sub-parts. """ if self.helperNode is not None: self.helperNode.finalize(True) self.helperNode = None self.helperObj = None def select(self): """ Visually select this part. """ if self.renderer is None: return self.helperObj.setMaterial(self.colliderSelectedMaterial) def deselect(self): """ Return back to deselected state """ if self.renderer is None or self.helperObj is None: return self.helperObj.setMaterial(self.colliderMaterial) def getDependencies(self): """ Returns a list of filenames that are dependencies of this part or its sub-parts """ if self.filename is not None: return [self.filename] return [] def getRadius(self): """ Returns the radius of the part including all sub-parts """ return 0 def getBoundingBox(self): """ Returns the bounding box of the part including all sub-parts """ return [Vector(), Vector()] def getColliders(self): """ There aren't any colliders for a mesh """ return [] def getSceneObjects(self): """ Returns all scene objects for this object """ return [self.obj] def isSameObject(self, entity): """ Returns true if this prefab has the entity in question. """ return False def onSize(self, _obj, _prop): """ Can't really size a light """ def _rebuild(self): """ If we've been built, rebuild now! """ if self.obj is None: return self.build(None, self.lastPosition, self.lastRotation, self.lastScale, renderer=self.renderer, parentNode=self.parentNode) def onMove(self, _obj, _prop): """ We've been moved! """ if self.node is None: return self.node.setPosition((self.x, self.y, self.z)) def onRotate(self, _obj, _prop): """ We've been rotated """ if self.node is None: return self.node.setOrientation(Quaternion.fromEuler((self.rx, self.ry, self.rz))) if self.kind == "directional" and self.helperObj is not None: self.helperNode.setPosition(Vector(-50, 0, 0) * Quaternion.fromEuler( (self.rx, self.ry, self.rz))) self.helperNode.setOrientation(Quaternion.fromEuler( (self.rx, self.ry, self.rz)) * -2.0) def onUpdateProperties(self, _obj, _prop): """ They've changed something. """ if self.obj is not None: self.obj.setSpecular(self.specular) self.obj.setDiffuse(self.diffuse) self.obj.setInnerAndOuter(self.inner, self.outer, self.falloff) class Prefab(ChangeNotifier): """ A prefab is a generic collection of parts. """ implements(IPart, IAnimatedMesh) parts = NotifierProperty("parts", None) name = NotifierProperty("name", "") filename = NotifierProperty("filename", None) position = NotifierProperty("position", None) rotation = NotifierProperty("rotation", None) scale = NotifierProperty("scale", None) x, y, z = vectorSetter("position") rx, ry, rz = vectorSetter("rotation", "r") sx, sy, sz = vectorSetter("scale", "s") node = None space = None colliderNode = None lightNode = None def __init__(self): self.parts = [] self.position = Vector() self.rotation = Vector() self.scale = Vector((1, 1, 1)) self.addPropertyListener("position", self.onMove) self.addPropertyListener("rotation", self.onRotate) self.addPropertyListener("scale", self.onSize) def __str__(self): return self.name or "Prefab" def build(self, conductor, position, rotation, scale, oid=None, body=None, space=None, renderer=None, parentNode=None, colliderArgs=None): """ Build the part and all sub-parts. Body and space are required for building colliders while renderer and parentNode are required for building visual parts. """ if self.node is not None and (renderer is not None or parentNode is not None): self.node.finalize(True) self.node = None physEngine = None if space is not None and hasattr(space, "world"): physEngine = space.world elif body is not None and hasattr(body, "world"): physEngine = body.world elif oid is not None: sim = conductor.getLocalService(ISimulationClient) physEngine = sim.getLocalRealm(oid[0]).world if physEngine is not None: self.space = physEngine.getPhysicsClass(ISpace)(physEngine) self.space.build(space=space) else: self.space = None position = Vector(position) + Quaternion(rotation) * (scale[0] * self.x, scale[1] * self.y, scale[2] * self.z) rotation = Quaternion(rotation) * Quaternion.fromEuler(self.rx, self.ry, self.rz) scale = Vector(scale[0] * self.scale[0], scale[1] * self.scale[1], scale[2] * self.scale[2]) if renderer is not None or parentNode is not None: if parentNode is None: parentNode = renderer.rootNode elif renderer is None: renderer = parentNode.renderer if self.node is None: self.node = renderer.getRendererClass(ISceneNode)(renderer, parentNode) self.node.initialize(str(id(self)) + self.name) self.node.setPosition(self.position) self.node.setOrientation(Quaternion.fromEuler(self.rotation)) self.node.setScale(self.scale) dfrds = [] for part in self.parts: res = part.build(conductor, position, rotation, scale, oid, body, self.space, renderer, self.node, colliderArgs) if isinstance(res, Deferred): dfrds.append(res) return gatherResults(dfrds) def destroy(self): """ Destroys the part and all sub-parts. """ for part in self.parts: part.destroy() if self.node is not None: self.node.finalize(True) self.node = None def setPosition(self, position): """ Currently only works for situations where the colliders are attached to a body. """ if self.node is not None: self.node.setPosition(position) def setOrientation(self, orientation): """ Currently only works for situations where the colliders are attached to a body. """ if self.node is not None: self.node.setOrientation(orientation) def clone(self): """ Returns a clone of this prefab """ cloned = Prefab() cloned.name = self.name cloned.filename = self.filename cloned.position = Vector(self.position) cloned.rotation = Vector(self.rotation) cloned.scale = Vector(self.scale) cloned.parts = [] for part in self.parts: cloned.parts.append(part.clone()) return cloned def showColliders(self, renderWindow=None, parentNode=None): """ Displays a visual representation of the colliders on this part and all sub-parts. """ if self.colliderNode is not None: self.hideColliders() if parentNode is None: parentNode = self.node#renderWindow.renderer.rootNode self.colliderNode = parentNode.renderer.getRendererClass(ISceneNode)( parentNode.renderer, parentNode.node) self.colliderNode.initialize("colliderNode%d" % id(self)) # self.colliderNode.setPosition(self.position) # self.colliderNode.setOrientation(Quaternion.fromEuler(self.rotation)) # self.colliderNode.setScale(self.scale) for part in self.parts: part.showColliders(renderWindow=renderWindow, parentNode=self.colliderNode) def hideColliders(self): """ Hides the visual representation of the colliders on this part and all sub-parts. """ for part in self.parts: part.hideColliders() if self.colliderNode is not None: self.colliderNode.finalize(True) self.colliderNode = None def showLights(self, renderWindow=None, parentNode=None): """ Displays a visual representation of any lights on this part and all sub-parts. """ if self.lightNode is not None: self.hideLights() if parentNode is None: parentNode = self.node self.lightNode = parentNode.renderer.getRendererClass(ISceneNode)( parentNode.renderer, parentNode.node) self.lightNode.initialize("lightNode%d" % id(self)) for part in self.parts: part.showLights(renderWindow=renderWindow, parentNode=self.lightNode) def hideLights(self): """ Hides the visual representation of any lights on this part and all sub-parts. """ if self.lightNode is not None: self.lightNode.finalize(True) self.lightNode = None for part in self.parts: part.hideLights() def select(self): """ Visually select this part. """ for part in self.parts: part.select() def deselect(self): """ Deselect the part. """ for part in self.parts: part.deselect() def save(self, root): """ Save this part to the given XML root object. """ prefab = SubElement(root, "Prefab", dict(name=self.name or "")) if self.filename is not None: prefab.attrib["filename"] = toRepresentationFilename(self.filename) makeVector(prefab, "position", self.position) makeVector(prefab, "rotation", self.rotation) makeVector(prefab, "scale", self.scale) if self.filename is not None: return for part in self.parts: part.save(prefab) @classmethod def load(cls, root): """ Load this part from the specified XML root object. """ prefab = cls() prefab.name = root.attrib.get("name", "") prefab.filename = fromRepresentationFilename(root.attrib.get("filename", None)) prefab.position = parseVector(root.find("position")) prefab.rotation = parseVector(root.find("rotation")) prefab.scale = parseVector(root.find("scale")) if prefab.filename: prefab.parts = readRepresentationFile(os.path.expanduser( os.path.join("~", ".mv3d", prefab.filename))) return prefab for part in root: ptype = part.tag if ptype in ["Box", "Sphere", "Cylinder", "Plane", "TriangleMesh"]: prefab.parts.append(Collider.load(part)) elif ptype in ["SpotLight", "PointLight", "DirectionalLight", "AmbientLight"]: prefab.parts.append(Light.load(part)) elif ptype == "Prefab": prefab.parts.append(Prefab.load(part)) elif ptype == "Mesh": prefab.parts.append(Mesh.load(part)) elif ptype in ["position", "rotation", "scale"]: continue else: raise TypeError("Invalid part type found: %s" % ptype) return prefab @classmethod def loadDotScene(cls, filename): """ Load a prefab from an Ogre .scene file """ root = parse(filename).getroot() nodes = root.find("nodes") parts = [] basePath = os.path.expanduser(os.path.join("~", ".mv3d")) + os.path.sep localDir = os.path.dirname(filename).replace(basePath, "") + os.path.sep for node in nodes: parts.append(cls._loadDotSceneNode(node, localDir)) prefab = cls() prefab.name = "dotScene" prefab.parts = parts return prefab @classmethod def _loadDotSceneNode(cls, node, localDir): """ Loads a node from a dotscene file """ prefab = cls() prefab.position = parseVector(node.find("position")) prefab.scale = parseVector(node.find("scale")) prefab.rotation = parseQuaternion(node.find("quaternion")).toEuler() prefab.parts = [] basePath = os.path.expanduser(os.path.join("~", ".mv3d")) + os.path.sep lastName = "Node" for tag in node: if tag.tag == "entity": mesh = Mesh() mesh.name = tag.attrib["name"] if mesh.name: lastName = mesh.name mesh.filename = localDir + tag.attrib["meshFile"] if os.path.exists(os.path.join(basePath, mesh.filename)): prefab.parts.append(mesh) else: print "Skipping missing mesh:", mesh.filename elif tag.tag == "node": prefab.parts.append(cls._loadDotSceneNode(tag, localDir)) elif tag.tag == "light": light = Light() light.name = tag.attrib["name"] if light.name: lastName = light.name light.kind = tag.attrib["type"] light.position = parseVector(tag.find("position")) light.diffuse = parseColor(tag.find("colourDiffuse")) light.specular = parseColor(tag.find("colourSpecular")) prefab.parts.append(light) prefab.name = node.attrib.get("name", lastName) return prefab def getDependencies(self): """ Returns a list of filenames that are dependencies of this part or its sub-parts """ deps = [] if self.filename is not None: return [self.filename] for part in self.parts: deps.extend(part.getDependencies()) return deps def getRadius(self): """ Returns the radius of the part including all sub-parts """ points = [] for part in self.parts: radius = part.getRadius() point = Vector(part.position) + Vector( part.position).normalize() * radius points.append(point.length()) return max(points) def getBoundingBox(self): """ Returns the bounding box of the part including all sub-parts """ points = [] for part in self.parts: points.extend(part.getBoundingBox()) xcoords = [] ycoords = [] zcoords = [] for point in points: xcoords.append(point[0]) ycoords.append(point[1]) zcoords.append(point[2]) return (min(xcoords), min(ycoords), min(zcoords)), (max(xcoords), max(ycoords), max(zcoords)) @classmethod @inlineCallbacks def loadFromAsset(cls, asvc, assetID): """ Loads the prefab from an asset """ asset = yield asvc.acquireAsset(assetID) ext = asset.getFullFile().split(".")[-1] prefab = cls() prefab.name = asset.getLocalFile() if ext == "rxml" or ext == "xml": prefab.parts = readRepresentationFile(asset.getFullFile()) elif ext == "obj": cfg = ConfigParser() cfg.read(asset.getFullFile()) for section in cfg.sections(): obj = Object() yield obj.load(cfg, section, asvc) prefab.parts.append(obj) elif ext == "solid": obj = Object() obj.loadFromAsset(asset) prefab.parts.append(obj) else: obj = Mesh() obj.name = "mesh" obj.filename = asset.getFullFile() obj.filename = obj.filename.replace(asvc.parent.dataRoot + os.path.sep, "") returnValue(obj) returnValue(prefab) def getColliders(self): """ Returns all the colliders in this object """ colliders = [] for part in self.parts: colliders.extend(part.getColliders()) return colliders def getSceneObjects(self): """ Returns all the scene objects in this object """ sceneObjects = [] for part in self.parts: sceneObjects.extend(part.getSceneObjects()) return sceneObjects def onMove(self, _obj, _prop): """ We've been moved! """ if self.node is not None: self.node.setPosition(self.position) for part in self.parts: part.onMove(None, None) def onRotate(self, _obj, _prop): """ We've been rotated """ if self.node is not None: self.node.setOrientation(Quaternion.fromEuler(self.rotation)) for part in self.parts: part.onMove(None, None) def onSize(self, _obj, _prop): """ We've been sized! """ if self.node is not None: self.node.setScale(self.scale) for part in self.parts: part.onSize(None, None) def isSameObject(self, entity): """ Returns true if this prefab has the entity in question. """ for part in self.parts: if part.isSameObject(entity): return True return False def getAnimations(self): """ Return a list of the animation names """ anims = [] for part in self.parts: if IAnimatedMesh.providedBy(part): anims.extend(part.getAnimations()) return list(set(anims)) def getActiveAnimations(self): """ Returns a list of the animations that are currently active. """ anims = [] for part in self.parts: if IAnimatedMesh.providedBy(part): anims.extend(part.getActiveAnimations()) return list(set(anims)) def getAnimationLength(self, animationName): """ Returns the length in seconds of the specified animation. """ length = 0.0 for part in self.parts: if IAnimatedMesh.providedBy(part): alen = part.getAnimationLength(animationName) if alen > length: length = alen return length def getAnimationPosition(self, animationName): """ Returns the current position in animation time for the animation. """ position = 0.0 for part in self.parts: if IAnimatedMesh.providedBy(part): pos = part.getAnimationPosition(animationName) if pos > position: position = pos return position def getAnimationSpeed(self, animationName): """ Returns the speed at which an animation is playing. """ speed = 0.0 for part in self.parts: if IAnimatedMesh.providedBy(part): aspeed = part.getAnimationSpeed(animationName) if aspeed > speed: speed = aspeed return speed def modifyActiveAnimation(self, name, position=None, loop=None, weight=None, speed=None): """ Modify properties of an active animation. """ for part in self.parts: if IAnimatedMesh.providedBy(part): part.modifyActiveAnimation(name, position, loop, weight, speed) def getBones(self): """ Returns a list of bones in the skeleton. """ bones = [] for part in self.parts: if IAnimatedMesh.providedBy(part): bones.extend(part.getBones()) return list(set(bones)) def createSocket(self, name, boneName, position, orientation): """ Creates a new named socket with the given position and orientation relative to the specified bone. """ for part in self.parts: if IAnimatedMesh.providedBy(part): part.createSocket(name, boneName, position, orientation) def attachToSocket(self, socketName, sceneObject): """ Attach a scene object to a socket. """ for part in self.parts: if IAnimatedMesh.providedBy(part): return part.attachToSocket(socketName, sceneObject) def detachFromSocket(self, socketName, sceneObject): """ Detach a previously attached scene object from a socket. """ for part in self.parts: if IAnimatedMesh.providedBy(part): return part.detachFromSocket(socketName, sceneObject) def startAnimation(self, name, loop=True, weight=100, speed=1.0): """ Start the named animation optionally looping and with the specified weight. """ for part in self.parts: if IAnimatedMesh.providedBy(part): part.startAnimation(name, loop, weight, speed) def stopAnimation(self, name): """ Stop a currently playing animation """ for part in self.parts: if IAnimatedMesh.providedBy(part): part.stopAnimation(name) class Representation(Persistable, Cacheable, Securable): """ Defines a single representation. """ _schemaVersion = 3 name = Text(autoSave=True, partialSave=True, transmit=True) assetID = IDTuple(autoSave=True, partialSave=True, transmit=True) position = FloatVector(autoSave=True, partialSave=True, transmit=True) orientation = FloatVector(autoSave=True, partialSave=True, transmit=True) scale = FloatVector(autoSave=True, partialSave=True, transmit=True) prefab = None parentSpace = None parentNode = None renderer = None conductor = None def __init__(self, store=None, **kwargs): Persistable.__init__(self, store, **kwargs) Securable.__init__(self) @classmethod def upgrade(cls, oldData): newData = oldData.copy() if oldData["_schemaVersion"] > 2: raise UpgradeError("Can't upgrade Representation from %d to %d" % ( oldData["_schemaVersion"], cls._schemaVersion)) oldVersion = oldData["_schemaVersion"] if oldVersion == 1: newData["order"] = 0 if oldVersion == 2: newData["name"] = "Representation" return newData def setName(self, name): """ Sets the name. """ self.name = name return self.updateAllClients("setName", name) def setPosition(self, position): """ Sets the position """ self.position = position d1 = maybeDeferred(self.rebuild) d2 = self.updateAllClients("setPosition", position) return DeferredList([d1, d2]) def setOrientation(self, orientation): """ Sets the orientation """ self.orientation = orientation d1 = maybeDeferred(self.rebuild) d2 = self.updateAllClients("setOrientation", orientation) return DeferredList([d1, d2]) def setScale(self, scale): """ Sets the scale """ self.scale = scale d1 = maybeDeferred(self.rebuild) d2 = self.updateAllClients("setScale", scale) return DeferredList([d1, d2]) def setAsset(self, assetID): """ Sets the asset id """ self.assetID = assetID d1 = maybeDeferred(self.rebuild) d2 = self.updateAllClients("setAsset", assetID) return DeferredList([d1, d2]) observe_setName = setName observe_setPosition = setPosition observe_setOrientation = setOrientation observe_setScale = setScale observe_setAsset = setAsset @inlineCallbacks def build(self, conductor, renderer=None, parentSpace=None, parentNode=None): """ Build in the given space/node. """ asvc = conductor.getLocalService(IAssetClient) prefab = yield Prefab.loadFromAsset(asvc, self.assetID) prefab.build(conductor, self.position, self.orientation, self.scale, space=self.parentSpace) self.prefab = prefab self.parentSpace = parentSpace self.parentNode = parentNode self.renderer = renderer self.conductor = conductor def rebuild(self): """ Rebuilds the representation. """ if self.conductor is None: return conductor = self.conductor renderer = self.renderer parentSpace = self.parentSpace parentNode = self.parentNode self.destroy() return self.build(conductor, renderer, parentSpace, parentNode) def destroy(self): """ Destroy the representation. """ if self.prefab is not None: self.prefab.destroy() self.conductor = None self.renderer = None self.parentSpace = None self.parentNode = None self.prefab = None pb.setUnjellyableForClass(Representation, Representation)