# -*- test-case-name: mv3d.test.test_factory -*- # Copyright (C) 2009-2012 Mortal Coil Games # See LICENSE for details. """ The main code for factories. A factory can generate objects, areas, etc in some form. This may or may not involve the input of a user. """ import random from cStringIO import StringIO from ConfigParser import ConfigParser from twisted.internet import defer from twisted.internet.defer import inlineCallbacks, returnValue from twisted.spread import pb from zope.interface import Interface, implements #@UnresolvedImport from mv3d.util.classgen import ClassGenerator from mv3d.util.conductor import findParent, IConductor from mv3d.phys.body import MobileBody, BasicVisualObject, StationaryBody from mv3d.server.sim import ISimulationService from mv3d.util.math3d import Vector, Quaternion from mv3d.phys.opende import ODEBox try: from mv3d.client.ui.ige.client import AssetSelector except ImportError: # TODO: put AssetSelector in a module that is not in client pass from mv3d.util.iservice import IAssetClient class IFactory(Interface): """ The basic interface for a factory. """ def giveInput(options): #@NoSelf """ Give this factory some input """ def getParameterInfo(parameter): #@NoSelf """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ def produce(): #@NoSelf """ Causes the factory to produce a product of some sort. """ class IFactoryWithUI(IFactory): """ This factory derivative produces things with the help of a user interface """ def getUI(): #@NoSelf """ Returns the class generator for the client side of the factory. """ def requiresInput(): #@NoSelf """ Returns true if the factory still requires more input before producing anything. To provide that input, construct the UI returned by getUI() """ class IWebFactory(IFactory): """ This defines a factory that gets its input from a web interface """ def coerceTuple(string, formatter=None): """ Parse a string representation of a tuple using formatter to reformat items within the tuple. If formatter isn't specified, int() is used """ formatter = formatter or int if isinstance(string, (Vector, Quaternion)) and formatter is float: return string if isinstance(string, tuple): return tuple([formatter(item) for item in string]) if string[0] != "(" or string[-1] != ")": raise ValueError("Malformatted tuple '%s'" % string) result = [] for value in string.strip("()").split(","): result.append(formatter(value)) return tuple(result) def coerceTupleTo(formatter=None): """ Handy method for returning a function that will coerce the tuple to a given type """ def function(string): """ The wrapper """ return coerceTuple(string, formatter) return function def coerceTupleList(string, formatter=None): """ Parse a list of tuples. The only concern here is that this is a naieve implementation that requires the comma to follow the ). """ formatter = formatter or int if isinstance(string, list): retval = [] for val in string: retval.append(coerceTuple(val, formatter)) return retval tuples = string.split("),") if len(tuples) == 1: return [coerceTuple(string, formatter)] listy = [] for tup in tuples: listy.append(coerceTuple(tup, formatter)) return listy def coerceClass(value): """ Turn a string value into a class generator. The class can be specified in one of two ways. It can be the full class name (mv3d.server.factory.BoxFactory), or it can be an assetid (0, 32). """ if isinstance(value, ClassGenerator): return value if value[0] == "(" and value[-1] == ")": return ClassGenerator(assetid=coerceTuple(value)) else: return ClassGenerator(value) def coerceFactory(option): """ Get a single or list of factories from the given string """ if isinstance(option, list): return option if not option.strip().startswith("[") or not option.strip().endswith("]"): raise ValueError("Malformatted factory option %s" % option) options = option.strip(" []").split(",") return [opt.strip() for opt in options] def coerceBool(value): """ Just simply make the value into a bool """ if isinstance(value, str): if value.lower().startswith("t") or value.lower().startswith("y"): return True else: return False return bool(value) def coerceQuaternionFromEuler(value): """ Coerce the value into a quaternion """ if isinstance(value, Quaternion): return value if isinstance(value, Vector): return Quaternion().fromEuler(value) if isinstance(value, tuple): if len(value) == 4: return Quaternion(value) elif len(value) == 3: return Quaternion().fromEuler(value) tup = coerceTuple(value, float) return Quaternion().fromEuler(tup) def filterAndOrderParameters(params, order=None, filters=None): """ Remove items in the filter list (plus coerce) and order by the order list if present """ order = order or params.keys() if filters is None: filters = ["coerce"] else: filters += ["coerce"] outParams = [] for param in order: outParams.append(params[param].copy()) outParams[-1]["name"] = param for filt in filters: if outParams[-1].has_key(filt): del outParams[-1][filt] return outParams class BoxFactory(pb.Viewable): """ A sample factory that creates boxes """ implements(IFactoryWithUI) position = None name = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 70, 0) ), name=dict( coerce=str, type="String", label="Name:", value="A Box", ), ) def __init__(self, parent): self.parent = parent def view_getInputSections(self, _client): """ Returns the input section definition for the UI """ return dict( main=filterAndOrderParameters(self.paramInfo, ["name", "position"])) def requiresInput(self): """ Check if the position is set """ return self.position is None or self.name is None def giveInput(self, options): """ Apply parameters to this factory """ if options.has_key("position"): self.position = options["position"] if options.has_key("name"): self.name = options["name"] def view_giveSectionInput(self, _client, _section, options): """ Filters over to giveInput since we only have one section. """ return self.giveInput(options) def getParameterInfo(self, param): """ Return a dict of info on the parameter """ return self.paramInfo[param] @inlineCallbacks def produce(self): """ Just create a box """ if self.requiresInput(): raise ValueError("Need more input!") area = yield self.parent.getItem((0, 0)) realm = area.realm box = yield area.newObject(ClassGenerator( "mv3d.server.model.physical.PhysicalBox")) box.create(realm) box.setName(self.name) box.setPosition(self.position) box.addVisualAsset((0, 6)) box.body.vobs[0].setScale((0.05, 0.05, 0.05)) area.takeOut(box.getID()) area.contain(box.getID()) def getUI(self): """ Return the class generator for the UI component to this. """ return ClassGenerator( "mv3d.client.ui.editor.Factory") class Object: """ A modifier that creates a basic physical object """ implements(IFactory) position = None rotation = None name = None body = None children = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 70, 0) ), rotation=dict( coerce=coerceQuaternionFromEuler, type="Position3D", value=(0, 0, 0) ), name=dict( coerce=str, type="String", label="Name:", value="Object", ), body=dict( coerce=coerceFactory, type="factory", ), children=dict( coerce=coerceFactory, type="factory", ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, self.paramInfo[option]["coerce"]( options[option])) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] @inlineCallbacks def produce(self): """ Causes the factory to produce a product of some sort. """ conductor = findParent(self.parent, IConductor) sim = conductor.getLocalService(ISimulationService) area = yield sim.getItem((0, 0)) realm = area.realm obj = yield area.newObject(ClassGenerator( "mv3d.server.model.physical.BasicPhysicalObject")) self.body[0].giveInput(dict(oid=obj.getID())) body = yield self.body[0].produce() body.create(realm.world) obj.setBody(body) # todo: area.newObject should set this? obj.setParent(sim) obj.create(realm) obj.setName(self.name) body.setPosition(self.body[0].position or (0, 0, 0)) if self.position is not None: obj.setPosition(self.position) if self.rotation is not None: obj.setRotation(self.rotation) if self.children is not None: # todo: this has to happen before the take out / put in # because adding a collider doesn't cause the collider to be # built right away. Change this. for child in self.children: child.parent = obj yield child.produce() area.takeOut(obj.getID()) area.contain(obj.getID()) obj.grantPermission("read", "all") obj.grantPermission("reference", "all") returnValue(obj) class BodyFactory: """ A factory that creates a Body """ implements(IFactory) position = None vobs = None colliders = None oid = None mobile = True world = None children = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 70, 0) ), oid=dict( coerce=coerceTuple, ), vobs=dict( coerce=coerceFactory, type="factory", ), colliders=dict( coerce=coerceFactory, type="factory", ), mobile=dict( coerce=coerceBool, type="Boolean", value=True, ), children=dict( coerce=coerceFactory, type="factory", ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, self.paramInfo[option]["coerce"]( options[option])) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] @inlineCallbacks def produce(self): """ Causes the factory to produce a product of some sort. """ if self.mobile: body = MobileBody(self.oid) else: body = StationaryBody() if self.vobs is not None: for vob in self.vobs: vob.parent = body yield vob.produce() if self.colliders is not None: for collider in self.colliders: collider.giveInput(dict(oid=self.oid)) collider.parent = body yield collider.produce() body.grantPermission("read", "all") body.grantPermission("reference", "all") if self.children is not None: for child in self.children: child.parent = body yield child.produce() returnValue(body) class Collider: """ A factory that creates a Collider """ implements(IFactory) position = None rotation = None shape = None size = None radius = None density = None oid = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 0, 0) ), rotation=dict( coerce=coerceQuaternionFromEuler, type="Position3D", value=(0, 0, 0) ), shape=dict( coerce=str, ), density=dict( coerce=float, ), size=dict( coerce=coerceTupleTo(float), type="Position3D", value=(1, 1, 1) ), oid=dict( coerce=coerceTuple, ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, options[option]) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] def produce(self): """ Causes the factory to produce a product of some sort. """ if self.shape == "box": collider = ODEBox(position=self.position, rotation=self.rotation or (1, 0, 0, 0), size=self.size, density=self.density) self.parent.addCollider(collider) return collider class BasicVob: """ A factory that creates a basic visual object """ implements(IFactory) position = None rotation = None scale = None assets = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 0, 0) ), rotation=dict( coerce=coerceQuaternionFromEuler, type="Position3D", value=(0, 0, 0) ), scale=dict( coerce=coerceTupleTo(float), type="Position3D", value=(1, 1, 1) ), assets=dict( coerce=coerceTupleList, ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, options[option]) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] def produce(self): """ Causes the factory to produce a product of some sort. """ vob = BasicVisualObject() vob.setOffset(self.position or (0, 0, 0), self.rotation or (1, 0, 0, 0)) vob.setScale(self.scale or (1, 1, 1)) if self.assets is not None: for aid in self.assets: vob.addAsset(aid) vob.grantPermission("read", "all") vob.grantPermission("reference", "all") self.parent.addVisualObject(vob) return vob class Random: """ A factory that returns random things """ implements(IFactory) factory = None resultCountRange = None numberRange = None strings = None boundingBox = None idList = None paramInfo = dict( factory=dict( coerce=coerceFactory, ), resultCountRange=dict( coerce=coerceTuple, value=(1, 1) ), numberRange=dict( coerce=coerceTupleTo(float), value=(0, 10) ), strings=dict( coerce=coerceTupleTo(str), value=tuple() ), boundingBox=dict( coerce=coerceTupleTo(float), value=(-1, -1, -1, 1, 1, 1) ), idList=dict( coerce=coerceTupleList, value=tuple() ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, options[option]) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] def produce(self): """ Causes the factory to produce a product of some sort. """ if self.factory is not None: chosen = [] for _ in range(random.randrange(self.resultCountRange[0], self.resultCountRange[1])): chosen.append(defer.maybeDeferred(random.choice( self.factory).produce)) return defer.gatherResults(chosen) elif self.numberRange is not None: chosen = [] for _ in range(random.randrange(self.resultCountRange[0], self.resultCountRange[1])): chosen.append(random.random() * (self.numberRange[1] - self.numberRange[0]) + self.numberRange[0]) if len(chosen) == 1: return chosen[0] return chosen elif self.strings is not None: chosen = [] for _ in range(random.randrange(self.resultCountRange[0], self.resultCountRange[1])): chosen.append(random.choice(self.strings)) if len(chosen) == 1: return chosen[0] return chosen elif self.boundingBox is not None: chosen = [] for _ in range(random.randrange(self.resultCountRange[0], self.resultCountRange[1])): coord = [] for pos in range(3): coord.append(random.random() * (self.boundingBox[pos + 3] - self.boundingBox[pos]) + self.boundingBox[pos]) chosen.append(tuple(coord)) if len(chosen) == 1: return chosen[0] return chosen elif self.idList is not None: chosen = [] for _ in range(random.randrange(self.resultCountRange[0], self.resultCountRange[1])): chosen.append(random.choice(self.idList)) if len(chosen) == 1: return chosen[0] return chosen class Chain: """ A factory that lines things up in a row. Items must have the following methods: * getBoundingBox * setPosition * setRotation """ implements(IFactory) position = None rotation = None spaceBetween = None items = None axis = None count = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 60, 0), label="Position:", ), rotation=dict( coerce=coerceQuaternionFromEuler, type="Position3D", value=(0, 0, 0), label="Rotation:", ), spaceBetween=dict( coerce=float, type="String", label="Spacing:", value=0, ), items=dict( coerce=coerceFactory, type="factory", ), axis=dict( coerce=str, type="String", value="x", label="Axis:", ), count=dict( coerce=float, type="String", label="Count:", ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, self.paramInfo[option]["coerce"]( options[option])) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] @inlineCallbacks def produce(self): """ Causes the factory to produce a product of some sort. """ cpos = Vector(self.position) items = [] next = 0 for cnt in range(self.count): itm = self.items[next] itm.giveInput(dict(position=cpos, rotation=self.rotation, name="Chain%d" % cnt)) self.items[next].parent = self.parent cur = yield self.items[next].produce() next += 1 if next == len(self.items): next = 0 bbox = cur.getBoundingBox() if "x" in self.axis.lower(): cpos += Vector(bbox[3] - bbox[0] + self.spaceBetween, 0, 0) if "y" in self.axis.lower(): cpos += Vector(0, bbox[4] - bbox[1] + self.spaceBetween, 0) if "z" in self.axis.lower(): cpos += Vector(0, 0, bbox[5] - bbox[2] + self.spaceBetween) items.append(cur) returnValue(items) class OffsetComposite: """ Basically, takes a bunch of children and a position and translates the children by the position and rotation before building them """ implements(IFactory) position = None rotation = None children = None paramInfo = dict( position=dict( coerce=coerceTupleTo(float), type="Position3D", value=(0, 60, 0), label="Position:", ), rotation=dict( coerce=coerceQuaternionFromEuler, type="Position3D", value=(0, 0, 0), label="Rotation:", ), spaceBetween=dict( coerce=float, type="String", label="Spacing:", value=0, ), children=dict( coerce=coerceFactory, type="factory", ), ) def __init__(self, parent): self.parent = parent def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, self.paramInfo[option]["coerce"]( options[option])) def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] @inlineCallbacks def produce(self): """ Produce the children but relocate them first """ for child in self.children: child.parent = self.parent position = child.position or (0, 0, 0) rotation = child.rotation or (1, 0, 0, 0) child.position = Vector(self.position or (0, 0, 0) ) + position child.rotation = Quaternion(self.rotation or (1, 0, 0, 0) ) * rotation yield child.produce() child.position = position child.rotation = rotation returnValue(self.parent) #? class Template(pb.Viewable): """ The template factory reads in an ini style file either from text input or from an asset and uses that to configure a tree of factories. """ implements(IFactoryWithUI) built = False paramInfo = dict( templateAsset=dict( coerce=coerceTuple, type="Asset", ), ) def __init__(self, parent): self.parent = parent self.template = ConfigParser() self.factories = {} def giveInput(self, options): """ Give this factory some input """ for option in self.paramInfo.keys(): if options.has_key(option): setattr(self, option, options[option]) def requiresInput(self): """ Returns true if no.5 needs more input """ if not len(self.template.sections()): return True for section in self.template.sections(): if self.template.has_option(section, "requireInput"): return True return False def getParameterInfo(self, parameter): """ This returns a dict of information about the requested parameter. Things like its expected type, range of acceptable values, special UI considerations, etc. """ return self.paramInfo[parameter] @inlineCallbacks def setTemplateAsset(self, assetid): """ Sets the template via an asset id """ cond = findParent(self.parent, IConductor) asvc = cond.getLocalService(IAssetClient) asset = yield asvc.acquireAsset(assetid) self.template.read(asset.getFullFile()) def view_setTemplate(self, _client, template): """ Sets the data of the template which we should load in to the config parser """ stio = StringIO(template) self.template.readfp(stio) @inlineCallbacks def build(self): """ Actually build the tree of factories """ topFactories = coerceFactory( self.template.get("factory", "parts").strip()) for factory in topFactories: yield self.buildFactory(self.parent, factory) self.built = True @inlineCallbacks def buildFactory(self, parent, name): """ Build a single factory from the config if it hasn't been built yet """ if self.factories.has_key(name): returnValue(self.factories[name]) classgen = coerceClass(self.template.get(name, "factory").strip()) self.factories[name] = yield classgen.construct(parent) self.setFactoryValues(name) returnValue(self.factories[name]) def giveInputToFactories(self, params): """ Give a parameters to all factories """ if not self.built: self.build() for factory in self.factories.values(): factory.giveInput(params) @inlineCallbacks def view_getInputSections(self, _client): """ Retrieve all of the input needed to build the UI """ if not self.built: yield self.build() sections = {} for name in self.factories.keys(): sections[name] = filterAndOrderParameters( self.gatherInputRequirements(name)) if sections[name] == dict(): del sections[name] returnValue(sections) def view_giveSectionInput(self, _client, section, options): """ Apply the options to the factory it belongs to """ self.factories[section].giveInput(options) def view_selectAsset(self, _client): """ Returns an asset selection dialog """ return AssetSelector(findParent(self, IConductor), []) def gatherInputRequirements(self, factory): """ Gather up the fields that input is required for and get info on them """ if not self.template.has_option(factory, "requireInput"): return dict() inputRequired = {} for item in self.template.get(factory, "requireInput").split(","): name = item.strip() inputRequired[name] = self.factories[factory].getParameterInfo(name) return inputRequired @inlineCallbacks def setFactoryValues(self, name): """ Runs through all the values in the factory's section and if they aren't special fields, it parses them and gives them to the factory as input. """ options = {} for option, value in self.template.items(name): if option in ["factory", "requireinput"]: continue options[option] = yield self.parseValue(self.factories[name], option, value) self.factories[name].giveInput(options) @inlineCallbacks def parseValue(self, parent, option, value): """ Attempt to parse a value by coercing it into a type """ info = parent.getParameterInfo(option) if info.get("type") != "factory": returnValue(info.get("coerce", str)(value)) factories = [] for factory in coerceFactory(value): fact = yield self.buildFactory(parent, factory) factories.append(fact) returnValue(factories) @inlineCallbacks def produce(self): """ Actually build the thing from the template """ if not self.built: yield self.build() topFactories = coerceFactory( self.template.get("factory", "parts").strip()) results = [] for factory in topFactories: result = yield self.factories[factory].produce() results.append(result) returnValue(results) def getUI(self): """ Returns the ui class generator """ return ClassGenerator("mv3d.client.ui.editor.Factory")