From f8fe9ef9f6edf1723f8309352706da2dc0eaece4 Mon Sep 17 00:00:00 2001 From: Gyedo Jeon Date: Fri, 5 Feb 2010 01:12:21 +0000 Subject: [PATCH] Added feature to import Maya file, undo/redo for object deletion --- direct/src/leveleditor/ActionMgr.py | 107 ++++++++++++-- direct/src/leveleditor/LevelEditorBase.py | 46 ++++-- direct/src/leveleditor/LevelEditorUI.py | 2 +- direct/src/leveleditor/MayaConverter.py | 154 ++++++++++++++++++++ direct/src/leveleditor/ObjectMgr.py | 46 ++++-- direct/src/leveleditor/ObjectPaletteBase.py | 5 +- direct/src/leveleditor/ObjectPropertyUI.py | 5 + direct/src/leveleditor/ProtoPaletteUI.py | 4 + 8 files changed, 329 insertions(+), 40 deletions(-) create mode 100755 direct/src/leveleditor/MayaConverter.py diff --git a/direct/src/leveleditor/ActionMgr.py b/direct/src/leveleditor/ActionMgr.py index e12a8ed4cb..d1723588a0 100755 --- a/direct/src/leveleditor/ActionMgr.py +++ b/direct/src/leveleditor/ActionMgr.py @@ -1,8 +1,20 @@ +import ObjectGlobals as OG + class ActionBase(Functor): """ Base class for user actions """ def __init__(self, function, *args, **kargs): Functor.__init__(self, function, *args, **kargs) + self.result = None + + def _do__call__(self, *args, **kargs): + self.saveStatus() + self.result = Functor._do__call__(self, *args, **kargs) + return self.result + + def saveStatus(self): + # save object status for undo here + pass def undo(self): print "undo method is not defined for this action" @@ -43,19 +55,92 @@ class ActionMgr: class ActionAddNewObj(ActionBase): """ Action class for adding new object """ - def __init__(self, function, *args, **kargs): + def __init__(self, editor, *args, **kargs): + self.editor = editor + function = self.editor.objectMgr.addNewObject ActionBase.__init__(self, function, *args, **kargs) - self.np = None - - def _do__call__(self, *args, **kargs): - self.np = ActionBase._do__call__(self, *args, **kargs) - return self.np def undo(self): - if self.np is None: + if self.result is None: print "Can't undo this" else: - base.direct.removeAllSelected() - base.le.objectMgr.deselectAll() - base.direct.removeNodePath(self.np) - self.np = None + base.direct.deselect(self.result) + base.direct.removeNodePath(self.result) + self.result = None + +class ActionDeleteObj(ActionBase): + """ Action class for deleting object """ + + def __init__(self, editor, *args, **kargs): + self.editor = editor + function = base.direct.removeAllSelected + ActionBase.__init__(self, function, *args, **kargs) + self.selectedUIDs = [] + self.hierarchy = {} + self.objInfos = {} + + def saveStatus(self): + selectedNPs = base.direct.selected.getSelectedAsList() + def saveObjStatus(np, isRecursive=True): + obj = self.editor.objectMgr.findObjectByNodePath(np) + if obj: + uid = obj[OG.OBJ_UID] + if not isRecursive: + self.selectedUIDs.append(uid) + objNP = obj[OG.OBJ_NP] + self.objInfos[uid] = obj + parentNP = objNP.getParent() + if parentNP == render: + self.hierarchy[uid] = None + else: + parentObj = self.editor.objectMgr.findObjectByNodePath(parentNP) + if parentObj: + self.hierarchy[uid] = parentObj[OG.OBJ_UID] + + for child in np.getChildren(): + if child.hasTag('OBJRoot'): + saveObjStatus(child) + + for np in selectedNPs: + saveObjStatus(np, False) + + def undo(self): + if len(self.hierarchy.keys()) == 0 or\ + len(self.objInfos.keys()) == 0: + print "Can't undo this" + else: + def restoreObject(uid, parentNP): + obj = self.objInfos[uid] + objDef = obj[OG.OBJ_DEF] + objModel = obj[OG.OBJ_MODEL] + objProp = obj[OG.OBJ_PROP] + objRGBA = obj[OG.OBJ_RGBA] + self.editor.objectMgr.addNewObject(objDef.name, + uid, + obj[OG.OBJ_MODEL], + parentNP) + self.editor.objectMgr.updateObjectColor(objRGBA[0], objRGBA[1], objRGBA[2], objRGBA[3], uid) + self.editor.objectMgr.updateObjectProperties(uid, objProp) + + while (len(self.hierarchy.keys()) > 0): + for uid in self.hierarchy.keys(): + if self.hierarchy[uid] is None: + parentNP = None + restoreObject(uid, parentNP) + del self.hierarchy[uid] + else: + parentObj = self.editor.objectMgr.findObjectById(self.hierarchy[uid]) + if parentObj: + parentNP = parentObj[OG.OBJ_NP] + restoreObject(uid, parentNP) + del self.hierarchy[uid] + + base.direct.deselectAll() + for uid in self.selectedUIDs: + obj = self.editor.objectMgr.findObjectById(uid) + if obj: + base.direct.select(obj[OG.OBJ_NP], fMultiSelect=1) + + self.selecteUIDs = [] + self.hierarchy = {} + self.objInfos = {} diff --git a/direct/src/leveleditor/LevelEditorBase.py b/direct/src/leveleditor/LevelEditorBase.py index 6631f35420..afac3e1459 100755 --- a/direct/src/leveleditor/LevelEditorBase.py +++ b/direct/src/leveleditor/LevelEditorBase.py @@ -16,6 +16,7 @@ from ObjectMgr import * from FileMgr import * from ActionMgr import * from ProtoPalette import * +from MayaConverter import * class LevelEditorBase(DirectObject): """ Base Class for Panda3D LevelEditor """ @@ -110,6 +111,7 @@ class LevelEditorBase(DirectObject): ('DIRECT-delete', self.handleDelete), ('preRemoveNodePath', self.removeNodePathHook), ('DIRECT_selectedNodePath_fMulti_fTag', self.selectedNodePathHook), + ('DIRECT_deselectedNodePath', self.deselectAll), ('DIRECT_deselectAll', self.deselectAll), ('LE-Undo', self.actionMgr.undo), ('LE-Redo', self.actionMgr.redo), @@ -140,20 +142,22 @@ class LevelEditorBase(DirectObject): base.direct.selected.last = None def handleDelete(self): - reply = wx.MessageBox("Do you want to delete selected?", "Delete?", - wx.YES_NO | wx.ICON_QUESTION) - if reply == wx.YES: - base.direct.removeAllSelected() - self.objectMgr.deselectAll() - else: - # need to reset COA - dnp = base.direct.selected.last - # Update camera controls coa to this point - # Coa2Camera = Coa2Dnp * Dnp2Camera - mCoa2Camera = dnp.mCoa2Dnp * dnp.getMat(base.direct.camera) - row = mCoa2Camera.getRow(3) - coa = Vec3(row[0], row[1], row[2]) - base.direct.cameraControl.updateCoa(coa) + action = ActionDeleteObj(self) + self.actionMgr.push(action) + action() +## reply = wx.MessageBox("Do you want to delete selected?", "Delete?", +## wx.YES_NO | wx.ICON_QUESTION) +## if reply == wx.YES: +## base.direct.removeAllSelected() +## else: +## # need to reset COA +## dnp = base.direct.selected.last +## # Update camera controls coa to this point +## # Coa2Camera = Coa2Dnp * Dnp2Camera +## mCoa2Camera = dnp.mCoa2Dnp * dnp.getMat(base.direct.camera) +## row = mCoa2Camera.getRow(3) +## coa = Vec3(row[0], row[1], row[2]) +## base.direct.cameraControl.updateCoa(coa) def selectedNodePathHook(self, nodePath, fMultiSelect = 0, fSelectTag = 1): # handle unpickable nodepath @@ -163,7 +167,7 @@ class LevelEditorBase(DirectObject): self.objectMgr.selectObject(nodePath) - def deselectAll(self): + def deselectAll(self, np=None): self.objectMgr.deselectAll() def reset(self): @@ -221,3 +225,15 @@ class LevelEditorBase(DirectObject): except: pass + + def convertMaya(self, modelname, obj=None, isAnim=False): + if obj and isAnim: + mayaConverter = MayaConverter(self.ui, self, modelname, obj, isAnim) + else: + reply = wx.MessageBox("Is it an animation file?", "Animation?", + wx.YES_NO | wx.ICON_QUESTION) + if reply == wx.YES: + mayaConverter = MayaConverter(self.ui, self, modelname, None, True) + else: + mayaConverter = MayaConverter(self.ui, self, modelname, None, False) + mayaConverter.Show() diff --git a/direct/src/leveleditor/LevelEditorUI.py b/direct/src/leveleditor/LevelEditorUI.py index ec5eac44a0..85581ade89 100755 --- a/direct/src/leveleditor/LevelEditorUI.py +++ b/direct/src/leveleditor/LevelEditorUI.py @@ -20,7 +20,7 @@ class PandaTextDropTarget(wx.TextDropTarget): self.editor = editor def OnDropText(self, x, y, text): - action = ActionAddNewObj(self.editor.objectMgr.addNewObject, text) + action = ActionAddNewObj(self.editor, text) self.editor.actionMgr.push(action) action() diff --git a/direct/src/leveleditor/MayaConverter.py b/direct/src/leveleditor/MayaConverter.py new file mode 100755 index 0000000000..dc6d0a508e --- /dev/null +++ b/direct/src/leveleditor/MayaConverter.py @@ -0,0 +1,154 @@ +from direct.wxwidgets.WxAppShell import * +import os, re, shutil + +from ObjectPaletteBase import * + +CLOSE_STDIN = "" + +class Process: + def __init__(self, parent, cmd, end_callback): + self.process = wx.Process(parent) + self.process.Redirect() + self.process.pid = wx.Execute(cmd, wx.EXEC_ASYNC|wx.EXEC_MAKE_GROUP_LEADER, self.process) + self.b = [] + if self.process.pid: + #what was up with wx.Process.Get*Stream names? + self.process._stdin_ = self.process.GetOutputStream() + self.process._stdout_ = self.process.GetInputStream() + self.process._stderr_ = self.process.GetErrorStream() + self.process.Bind(wx.EVT_END_PROCESS, end_callback) + return + raise StartupError + + def Poll(self, input=''): + if (input or self.b) and self.process and self.process._stdin_: + if self.b or len(input) > 512: + if input: + #if we don't chop up our input into resonably sized chunks, + #some platforms (like Windows) will send some small number + #of bytes per .write() call (sometimes 2 in the case of + #Windows). + self.b.extend([input[i:i+512] for i in xrange(0, len(input), 512)]) + input = self.b.pop(0) + self.process._stdin_.write(input) + if hasattr(self.process._stdin_, "LastWrite"): + y = self.process._stdin_.LastWrite() + if y != len(input): + self.b.insert(0, input[y:]) + x = [] + for s in (self.process._stderr_, self.process._stdout_): + if s and s.CanRead(): + x.append(s.read()) + else: + x.append('') + return x + + def CloseInp(self): + if self.process and self.process._stdin_: + self.process.CloseOutput() + self.process._stdin_ = None + + def Kill(self, ks='SIGKILL'): + errors = {wx.KILL_BAD_SIGNAL: "KILL_BAD_SIGNAL", + wx.KILL_ACCESS_DENIED: "KILL_ACCESS_DENIED", + wx.KILL_ERROR: "KILL_ERROR"} + if self.process: + if ks == CLOSE_STDIN: + self.CloseInp() + return 1, None + elif wx.Process.Exists(self.process.pid): + signal = getattr(wx, ks) + r = wx.Process.Kill(self.process.pid, signal, flags=wx.KILL_CHILDREN) + else: + r = 65535 + self.CloseInp() + return 1, None + + if r not in (wx.KILL_OK, wx.KILL_NO_PROCESS, 65535): + return 0, (self.process.pid, signal, errors.get(r, "UNKNOWN_KILL_ERROR %s"%r)) + else: + return 1, None + +class MayaConverter(wx.Dialog): + def __init__(self, parent, editor, mayaFile, obj = None, isAnim=False): + wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Maya Converter", + pos=wx.DefaultPosition, size=(300, 200)) + + self.editor = editor + self.obj = obj + self.isAnim = isAnim + + self.mainPanel = wx.Panel(self, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.mainPanel, 1, wx.EXPAND, 0) + self.SetSizer(sizer) + + self.output = wx.TextCtrl(self.mainPanel, -1, style = wx.TE_MULTILINE, pos = (0, 0), size = (100, 400)) + sizer2 = wx.BoxSizer(wx.VERTICAL) + sizer2.Add(self.output, 1, wx.EXPAND, 0) + self.mainPanel.SetSizer(sizer2) + + if self.isAnim: + if self.obj: + command = 'maya2egg -a chan %s -o %s.anim.egg'%(mayaFile, mayaFile) + self.process = Process(self, command, lambda p0=None, p1=mayaFile: self.onProcessEnded(p0, p1)) + else: + command = 'maya2egg -a model %s -o %s.model.egg'%(mayaFile, mayaFile) + self.process = Process(self, command, lambda p0=None, p1=mayaFile: self.onModelProcessEnded(p0, p1)) + else: + command = 'maya2egg %s -o %s.egg'%(mayaFile, mayaFile) + self.process = Process(self, command, lambda p0=None, p1=mayaFile: self.onProcessEnded(p0, p1)) + + self.timer = wx.Timer(self, -1) + self.Bind(wx.EVT_TIMER, self.onPoll, self.timer) + self.timer.Start(100) + + def onPoll(self, evt): + if self.process: + for i in self.process.Poll(): + self.output.AppendText(i) + + def onModelProcessEnded(self, evt, mayaFile): + self.process.CloseInp() + for i in self.process.Poll(): + self.output.AppendText(i) + self.process = None + command = 'maya2egg -a chan %s -o %s.anim.egg'%(mayaFile, mayaFile) + self.process = Process(self, command, lambda p0 = None, p1=mayaFile: self.onProcessEnded(p0, p1)) + + def onProcessEnded(self, evt, mayaFile): + self.process.CloseInp() + for i in self.process.Poll(): + self.output.AppendText(i) + + self.output.AppendText('Converting %s is finished\n'%mayaFile) + self.process = None + + name = os.path.basename(mayaFile) + if self.isAnim: + if self.obj: + objDef = self.obj[OG.OBJ_DEF] + objNP = self.obj[OG.OBJ_NP] + animName = "%s.anim.egg"%mayaFile + if animName not in objDef.anims: + objDef.anims.append(animName) + name = os.path.basename(animName) + objNP.loadAnims({name:animName}) + objNP.loop(name) + self.obj[OG.OBJ_ANIM] = animName + self.editor.ui.objectPropertyUI.updateProps(self.obj) + return + else: + modelName = "%s.model.egg"%mayaFile + animName = "%s.anim.egg"%mayaFile + itemData = ObjectBase(name=name, model=modelName, anims=[animName], actor=True) + else: + modelName = "%s.egg"%mayaFile + itemData = ObjectBase(name=name, model=modelName, actor=False) + + self.editor.protoPalette.add(itemData) + + newItem = self.editor.ui.protoPaletteUI.tree.AppendItem(self.editor.ui.protoPaletteUI.root, name) + self.editor.ui.protoPaletteUI.tree.SetItemPyData(newItem, itemData) + self.editor.ui.protoPaletteUI.tree.ScrollTo(newItem) + diff --git a/direct/src/leveleditor/ObjectMgr.py b/direct/src/leveleditor/ObjectMgr.py index e77a9d4673..9ac41f3d8e 100755 --- a/direct/src/leveleditor/ObjectMgr.py +++ b/direct/src/leveleditor/ObjectMgr.py @@ -58,7 +58,7 @@ class ObjectMgr: self.lastUidMod = 0 return newUid - def addNewObject(self, typeName, uid = None, model = None, parent=None, fSelectObject=True): + def addNewObject(self, typeName, uid = None, model = None, parent=None, anim = None, fSelectObject=True): """ function to add new obj to the scene """ if parent is None: parent = render @@ -105,7 +105,6 @@ class ObjectMgr: except: newobj = loader.loadModel(Filename.fromOsSpecific(model).getFullpath()) - anim = '' i = 0 for i in range(len(objDef.anims)): animFile = objDef.anims[i] @@ -114,9 +113,14 @@ class ObjectMgr: if i < len(objDef.animNames): animName = objDef.animNames[i] newAnim = newobj.loadAnims({animName:animFile}) - if i == 0: - anim = animFile - newobj.loop(animName) + + if anim: + if anim == animFile: + newobj.loop(animName) + else: + if i == 0: + anim = animFile + newobj.loop(animName) if newobj is None: return None @@ -151,6 +155,11 @@ class ObjectMgr: del self.objects[uid] del self.npIndex[nodePath] + # remove children also + for child in nodePath.getChildren(): + if child.hasTag('OBJRoot'): + self.removeObjectByNodePath(child) + def findObjectById(self, uid): return self.objects.get(uid) @@ -290,7 +299,8 @@ class ObjectMgr: base.direct.deselectAll() objNP = obj[OG.OBJ_NP] - + objRGBA = obj[OG.OBJ_RGBA] + # load new model newobj = loader.loadModel(model) newobj.setTag('OBJRoot','1') @@ -306,6 +316,9 @@ class ObjectMgr: newobj.setHpr(objNP.getHpr()) newobj.setScale(objNP.getScale()) + # copy RGBA data + self.updateObjectColor(objRGBA[0], objRGBA[1], objRGBA[2], objRGBA[3], newobj) + # delete old geom del self.npIndex[NodePath(objNP)] objNP.removeNode() @@ -474,6 +487,7 @@ class ObjectMgr: np = obj[OG.OBJ_NP] objDef = obj[OG.OBJ_DEF] objModel = obj[OG.OBJ_MODEL] + objAnim = obj[OG.OBJ_ANIM] objProp = obj[OG.OBJ_PROP] objRGBA = obj[OG.OBJ_RGBA] @@ -482,10 +496,17 @@ class ObjectMgr: else: parentStr = "None" - if objModel is None: - self.saveData.append("\nobjects['%s'] = objectMgr.addNewObject('%s', '%s', None, %s)"%(uid, objDef.name, uid, parentStr)) + if objModel: + modelStr = "'%s'"%objModel else: - self.saveData.append("\nobjects['%s'] = objectMgr.addNewObject('%s', '%s', '%s', %s)"%(uid, objDef.name, uid, objModel, parentStr)) + modelStr = "None" + + if objAnim: + animStr = "'%s'"%objAnim + else: + animStr = "None" + + self.saveData.append("\nobjects['%s'] = objectMgr.addNewObject('%s', '%s', %s, %s, %s)"%(uid, objDef.name, uid, modelStr, parentStr, animStr)) self.saveData.append("if objects['%s']:"%uid) self.saveData.append(" objects['%s'].setPos(%s)"%(uid, np.getPos())) self.saveData.append(" objects['%s'].setHpr(%s)"%(uid, np.getHpr())) @@ -505,6 +526,10 @@ class ObjectMgr: if obj is None: return None objDef = obj[OG.OBJ_DEF] + objModel = obj[OG.OBJ_MODEL] + objAnim = obj[OG.OBJ_ANIM] + objRGBA = obj[OG.OBJ_RGBA] + if parent is None: parent = nodePath.getParent() @@ -521,6 +546,9 @@ class ObjectMgr: # copy model info self.updateObjectModel(obj[OG.OBJ_MODEL], newObj, fSelectObject=False) + # copy anim info + self.updateObjectAnim(obj[OG.OBJ_ANIM], newObj, fSelectObject=False) + # copy other properties for key in obj[OG.OBJ_PROP]: self.updateObjectPropValue(newObj, key, obj[OG.OBJ_PROP][key]) diff --git a/direct/src/leveleditor/ObjectPaletteBase.py b/direct/src/leveleditor/ObjectPaletteBase.py index fc2051df9f..805e216566 100755 --- a/direct/src/leveleditor/ObjectPaletteBase.py +++ b/direct/src/leveleditor/ObjectPaletteBase.py @@ -38,11 +38,8 @@ class ObjectPaletteBase: """ You can insert item to obj palette tree. - 'item' is the name to be inserted, it can be either a group or obj. - If item is a group 'obj' will be None. + 'item' is the object to be inserted, it can be either a group or obj. 'parentName' is the name of parent under where this item will be inserted. - 'data' is used in recursive searching. - """ if type(self.data) != dict: return None diff --git a/direct/src/leveleditor/ObjectPropertyUI.py b/direct/src/leveleditor/ObjectPropertyUI.py index 8dd59f69cd..776621db16 100755 --- a/direct/src/leveleditor/ObjectPropertyUI.py +++ b/direct/src/leveleditor/ObjectPropertyUI.py @@ -29,6 +29,11 @@ class AnimFileDrop(wx.FileDropTarget): for filename in filenames: name = os.path.basename(filename) animName = Filename.fromOsSpecific(filename).getFullpath() + if name.endswith('.mb') or\ + name.endswith('.ma'): + self.editor.convertMaya(animName, obj, isAnim=True) + return + if animName not in objDef.anims: objDef.anims.append(animName) diff --git a/direct/src/leveleditor/ProtoPaletteUI.py b/direct/src/leveleditor/ProtoPaletteUI.py index 460d818fbc..d54f9bbe2a 100755 --- a/direct/src/leveleditor/ProtoPaletteUI.py +++ b/direct/src/leveleditor/ProtoPaletteUI.py @@ -21,6 +21,10 @@ class FileDrop(wx.FileDropTarget): return modelname = Filename.fromOsSpecific(filename).getFullpath() + if modelname.endswith('.mb') or\ + modelname.endswith('.ma'): + self.editor.convertMaya(modelname) + return itemData = ObjectBase(name=name, model=modelname, actor=True) self.editor.protoPalette.add(itemData)