diff --git a/Jenkinsfile b/Jenkinsfile index b21ea0b5c..46ffe0a71 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -119,10 +119,6 @@ pipeline { steps { script { sh "rm -rf ${env.WORKSPACE}/../mhx2-makehuman-exchange" - sh "rm -rf ${env.WORKSPACE}/../community-plugins-assetdownload" - sh "rm -rf ${env.WORKSPACE}/../community-plugins-mhapi" - sh "rm -rf ${env.WORKSPACE}/../community-plugins-socket" - sh "rm -rf ${env.WORKSPACE}/../community-plugins-massproduce" sh "rm -rf ${env.WORKSPACE}/../community-plugins-makeclothes" sh "rm -rf ${env.WORKSPACE}/../community-plugins-makeskin" sh "rm -rf ${env.WORKSPACE}/../community-plugins-maketarget" @@ -149,10 +145,6 @@ pipeline { script { sh "pwd" sh "git clone https://github.com/makehumancommunity/mhx2-makehuman-exchange ${env.WORKSPACE}/../mhx2-makehuman-exchange" - sh "git clone https://github.com/makehumancommunity/community-plugins-assetdownload ${env.WORKSPACE}/../community-plugins-assetdownload" - sh "git clone https://github.com/makehumancommunity/community-plugins-mhapi ${env.WORKSPACE}/../community-plugins-mhapi" - sh "git clone https://github.com/makehumancommunity/community-plugins-socket ${env.WORKSPACE}/../community-plugins-socket" - sh "git clone https://github.com/makehumancommunity/community-plugins-massproduce ${env.WORKSPACE}/../community-plugins-massproduce" } } diff --git a/buildscripts/win32/makePynsistBuild.py b/buildscripts/win32/makePynsistBuild.py index d46e543e3..9ed6656bf 100644 --- a/buildscripts/win32/makePynsistBuild.py +++ b/buildscripts/win32/makePynsistBuild.py @@ -140,37 +140,37 @@ else: print("MHX2 was not found in parent directory: " + mhx2) -asset = os.path.abspath(os.path.join(parentdir,'community-plugins-assetdownload')) -if os.path.exists(asset): - tocopy = os.path.abspath(os.path.join(asset,'8_asset_downloader')) - todest = os.path.abspath(os.path.join(pluginsdir,'8_asset_downloader')) - copy_tree(tocopy, todest) -else: - print("asset downloader was not found in parent directory: " + asset) - -mhapi = os.path.abspath(os.path.join(parentdir,'community-plugins-mhapi')) -if os.path.exists(mhapi): - tocopy = os.path.abspath(os.path.join(mhapi,'1_mhapi')) - todest = os.path.abspath(os.path.join(pluginsdir,'1_mhapi')) - copy_tree(tocopy, todest) -else: - print("MHAPI was not found in parent directory: " + mhapi) - -socket = os.path.abspath(os.path.join(parentdir,'community-plugins-socket')) -if os.path.exists(socket): - tocopy = os.path.abspath(os.path.join(socket,'8_server_socket')) - todest = os.path.abspath(os.path.join(pluginsdir,'8_server_socket')) - copy_tree(tocopy, todest) -else: - print("socket plugin was not found in parent directory: " + socket) - -mp = os.path.abspath(os.path.join(parentdir,'community-plugins-massproduce')) -if os.path.exists(mp): - tocopy = os.path.abspath(os.path.join(mp,'9_massproduce')) - todest = os.path.abspath(os.path.join(pluginsdir,'9_massproduce')) - copy_tree(tocopy, todest) -else: - print("mass produce plugin was not found in parent directory: " + mp) +# asset = os.path.abspath(os.path.join(parentdir,'community-plugins-assetdownload')) +# if os.path.exists(asset): +# tocopy = os.path.abspath(os.path.join(asset,'8_asset_downloader')) +# todest = os.path.abspath(os.path.join(pluginsdir,'8_asset_downloader')) +# copy_tree(tocopy, todest) +# else: +# print("asset downloader was not found in parent directory: " + asset) +# +# mhapi = os.path.abspath(os.path.join(parentdir,'community-plugins-mhapi')) +# if os.path.exists(mhapi): +# tocopy = os.path.abspath(os.path.join(mhapi,'1_mhapi')) +# todest = os.path.abspath(os.path.join(pluginsdir,'1_mhapi')) +# copy_tree(tocopy, todest) +# else: +# print("MHAPI was not found in parent directory: " + mhapi) +# +# socket = os.path.abspath(os.path.join(parentdir,'community-plugins-socket')) +# if os.path.exists(socket): +# tocopy = os.path.abspath(os.path.join(socket,'8_server_socket')) +# todest = os.path.abspath(os.path.join(pluginsdir,'8_server_socket')) +# copy_tree(tocopy, todest) +# else: +# print("socket plugin was not found in parent directory: " + socket) +# +# mp = os.path.abspath(os.path.join(parentdir,'community-plugins-massproduce')) +# if os.path.exists(mp): +# tocopy = os.path.abspath(os.path.join(mp,'9_massproduce')) +# todest = os.path.abspath(os.path.join(pluginsdir,'9_massproduce')) +# copy_tree(tocopy, todest) +# else: +# print("mass produce plugin was not found in parent directory: " + mp) subprocess.call(["pynsist", "pynsist.cfg"], cwd=exportDir) diff --git a/makehuman/plugins/1_mhapi/JsonCall.py b/makehuman/plugins/1_mhapi/JsonCall.py new file mode 100644 index 000000000..2820d6828 --- /dev/null +++ b/makehuman/plugins/1_mhapi/JsonCall.py @@ -0,0 +1,113 @@ +#!/usr/bin/python + +import json +import numpy as np +# import socket + + +class JsonCall(): + + def __init__(self, jsonData=None): + self.params = {} + self.data = None + self.function = "generic" + self.error = "" + self.responseIsBinary = False + + if jsonData: + self.initializeFromJson(jsonData) + + def initializeFromJson(self, jsonData): + j = json.loads(jsonData) + if not j: + return + self.function = j["function"] + self.error = j["error"] + if j["params"]: + for key, value in j["params"].items(): + self.params[key] = value + if j["data"]: + self.data = j["data"] + + def setData(self, data=""): + self.data = data + + def getData(self): + return self.data + + def setParam(self, name, value): + self.params[name] = value + + def getParam(self, name): + return self.params.get(name, None) + + def setFunction(self, func): + self.function = func + + def getFunction(self): + return self.function + + def setError(self, error): + self.error = error + + def getError(self): + return self.error + + def serialize(self): + + data = {'function': self.function, + 'error': self.error, + 'params': self.params, + 'data': self.data + } + + return json.dumps(data, cls=MHApiEncoder) + +# def send(self, host = "127.0.0.1", port = 12345): +# client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# client.connect((host, port)) +# client.send(bytes(self.serialize(),encoding='utf-8')) +# +# data = "" +# +# while True: +# buf = client.recv(1024) +# if len(buf) > 0: +# data += buf.strip().decode('utf-8') +# else: +# break +# +# if data: +# return JsonCall(data) +# else: +# return None + + +class MHApiEncoder(json.JSONEncoder): + + def default(self, obj): + + if isinstance(obj, np.ndarray): + if obj.dtype == np.dtype('bool'): + return obj.tolist() + else: + return obj.round(6).tolist() + + if isinstance(obj, bytes): + return str(obj, encoding='utf-8') + + if isinstance(obj, float) or isinstance(obj, np.float32) or isinstance(obj, np.float64): + return float(round(obj, 6)) + + if isinstance(obj, np.integer): + return int(obj) + + # suggested by Python + # try: + # iterable = iter(obj) + # except TypeError: + # pass + # else: + # return list(iterable) + + return json.JSONEncoder.default(self, obj) diff --git a/makehuman/plugins/1_mhapi/__init__.py b/makehuman/plugins/1_mhapi/__init__.py new file mode 100644 index 000000000..5621ef8b4 --- /dev/null +++ b/makehuman/plugins/1_mhapi/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/python + +__all__ = ["api","namespace","JsonCall"] + +from .api import API + +def load(app): + app.mhapi = API(app) + +def unload(app): + pass + diff --git a/makehuman/plugins/1_mhapi/_assets.py b/makehuman/plugins/1_mhapi/_assets.py new file mode 100644 index 000000000..509770698 --- /dev/null +++ b/makehuman/plugins/1_mhapi/_assets.py @@ -0,0 +1,632 @@ +#!/usr/bin/python + +from .namespace import NameSpace + +import getpath +import os +import sys +import re +import shutil +import glob +import fnmatch +import proxy +import gui3d + +from core import G + +class Assets(NameSpace): + """This namespace wraps all calls that are related to reading and managing assets.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + + self.assetTypes = ["material", + "model", + "clothes", + "hair", + "teeth", + "eyebrows", + "eyelashes", + "tongue", + "eyes", + "proxy", + "skin", + "pose", + "expression", + "rig", + "target", + "node_setups_and_blender_specific"] + + self.extensionToType = dict() + self.extensionToType[".mhmat"] = "material" + self.extensionToType[".mhclo"] = "proxy" + self.extensionToType[".proxy"] = "proxy" + self.extensionToType[".target"] = "target" + self.extensionToType[".mhm"] = "models" + + self.typeToExtension = {'material' : 'mhmat', + 'models' : 'mhm', + 'model' : 'mhm', + 'clothes' : 'mhclo', + 'hair' : 'mhclo', + 'teeth' : 'mhclo', + 'eyebrows' : 'mhclo', + 'eyelashes' : 'mhclo', + 'tongue' : 'mhclo', + 'eyes' : 'mhclo', + 'proxymeshes': 'proxy', + 'target' : 'target', + 'skin' : 'mhmat'} + + self.genericExtraKeys = ["tag"] + self.genericKeys = ["name","description", "uuid"] + self.genericCommentKeys = ["license","homepage","author"] + + self.proxyKeys = [ + "basemesh", + "obj_file", + "max_pole", + "material", + "z_depth", + "x_scale", + "y_scale", + "z_scale" + ] + + self.materialKeys = [ + "diffuseColor", + "specularColor", + "emissiveColor", + "ambientColor", + "diffuseTexture", + "bumpmapTexture", + "normalmapTexture", + "displacementmapTexture", + "specularmapTexture", + "transparencymapTexture", + "aomapTexture", + "diffuseIntensity", + "bumpMapIntensity", + "normalMapIntensity", + "displacementMapIntensity", + "specularMapIntensity", + "transparencyMapIntensity", + "aoMapIntensity", + "shininess", + "opacity", + "translucency", + "shadeless", + "wireframe", + "transparent", + "alphaToCoverage", + "backfaceCull", + "depthless", + "castShadows", + "receiveShadows", + + ] # There are also SSS settings, but I don't know if those actually works + + self.keyList = self.genericExtraKeys + self.genericCommentKeys + self.genericKeys +self.materialKeys + \ + self.proxyKeys + + self.zDepth = {"Body": 31, + "Underwear and lingerie": 39, + "Socks and stockings": 43, + "Shirt and trousers": 47, + "Sweater": 50, + "Indoor jacket": 53, + "Shoes and boots": 57, + "Coat": 61, + "Backpack": 69 + } + + def _parseGenericAssetInfo(self,fullPath): + + info = dict() + + fPath, ext = os.path.splitext(fullPath) + basename = os.path.basename(fullPath) + + info["type"] = self.extensionToType[ext] + info["absolute path"] = fullPath + info["extension"] = ext + info["basename"] = basename + info["rawlines"] = [] + info["location"] = os.path.dirname(fullPath) + info["parentdir"] = os.path.basename(info["location"]) + + with open(fullPath, 'r', encoding='utf8') as f: + contents = f.readlines() + for line in contents: + info["rawlines"].append(re.sub(r"[\x0a\x0d]+",'',line)) + + info["rawkeys"] = [] + info["rawcommentkeys"] = [] + + for line in info["rawlines"]: + m = re.match(r"^([a-zA-Z_]+)\s+(.*)$",line) + if m: + info["rawkeys"].append([m.group(1),m.group(2)]) + m = re.match(r"^#\s+([a-zA-Z_]+)\s+(.*)$",line) + if m: + info["rawcommentkeys"].append([m.group(1),m.group(2)]) + + for genericExtraKeyName in self.genericExtraKeys: + info[genericExtraKeyName] = set() + for rawkey in info["rawkeys"]: + rawKeyName = rawkey[0] + rawKeyValue = rawkey[1] + if rawKeyName == genericExtraKeyName: + info[genericExtraKeyName].add(rawKeyValue) + + for genericKeyName in self.genericKeys: + info[genericKeyName] = None + for rawkey in info["rawkeys"]: + rawKeyName = rawkey[0] + rawKeyValue = rawkey[1] + if rawKeyName == genericKeyName: + info[genericKeyName] = rawKeyValue + + for genericCommentKeyName in self.genericCommentKeys: + info[genericCommentKeyName] = None + for commentKey in info["rawcommentkeys"]: + commentKeyName = commentKey[0] + commentKeyValue = commentKey[1] + if commentKeyName == genericCommentKeyName: + info[commentKeyName] = commentKeyValue + + return info + + def _parseProxyKeys(self,assetInfo): + for pk in self.proxyKeys: + assetInfo[pk] = None + for k in assetInfo["rawkeys"]: + key = k[0] + value = k[1] + if key == pk: + assetInfo[pk] = value + + def _parseMaterialKeys(self,assetInfo): + for pk in self.materialKeys: + assetInfo[pk] = None + for k in assetInfo["rawkeys"]: + key = k[0] + value = k[1] + if key == pk: + assetInfo[pk] = value + + def _addPertinentKeyInfo(self,assetInfo): + + pertinentKeys = list(self.genericKeys) + pertinentExtraKeys = list(self.genericExtraKeys) + pertinentCommentKeys = list(self.genericCommentKeys) + + if assetInfo["type"] == "proxy": + pertinentKeys.extend(self.proxyKeys) + + if assetInfo["type"] == "material": + pertinentKeys.extend(self.materialKeys) + + assetInfo["pertinentKeys"] = pertinentKeys + assetInfo["pertinentExtraKeys"] = pertinentExtraKeys + assetInfo["pertinentCommentKeys"] = pertinentCommentKeys + + def assetTitleToDirName(self, assetTitle): + """Convert an asset title (as shown for example in a list) to a normalized file name""" + normalizedTitle = assetTitle.strip() + normalizedTitle = re.sub(r'_+', ' ', normalizedTitle) + normalizedTitle = normalizedTitle.strip() + normalizedTitle = re.sub(r'\s+', '_', normalizedTitle) + normalizedTitle = re.sub(r'[*:,\[\]/\\\(\)]+', '', normalizedTitle) + return normalizedTitle + + def getAssetTypes(self): + """Returns a non-live list of known asset types""" + return list(self.assetTypes) + + def getAssetLocation(self, assetTitle, assetType): + """Get the full normal (user) path for an asset based on its title and type""" + alreadyKosher = ["clothes", + "hair", + "teeth", + "eyebrows", + "eyelashes", + "tongue", + "eyes"] + + needsPlural = ["material", + "model", + "skin", + "pose", + "expression", + "rig"] + + normalizedTitle = self.assetTitleToDirName(assetTitle) + + if assetType == "model": + root = self.api.locations.getUserHomePath("models") + return os.path.join(root, normalizedTitle) + + if assetType in alreadyKosher: + root = self.api.locations.getUserDataPath(assetType) + return os.path.join(root,normalizedTitle) + + if assetType in needsPlural: + root = self.api.locations.getUserDataPath(assetType + "s") + return os.path.join(root,normalizedTitle) + + if assetType == "proxy": + return self.api.locations.getUserDataPath("proxymeshes") + + if assetType == "target": + return self.api.locations.getUserDataPath("custom") + + if assetType == "model": + return self.api.locations.getUserHomePath("models") + + raise ValueError("Could not convert title to location for asset with type",assetType) + + def openAssetFile(self, path, strip = False): + """Opens an asset file and returns a hash describing it""" + fullPath = self.api.locations.getUnicodeAbsPath(path) + if not os.path.isfile(fullPath): + return None + info = self._parseGenericAssetInfo(fullPath) + + self._addPertinentKeyInfo(info) + + if info["type"] == "proxy": + self._parseProxyKeys(info) + + if info["type"] == "material": + self._parseMaterialKeys(info) + + thumbPath = os.path.splitext(path)[0] + ".thumb" + + if os.path.isfile(thumbPath): + info["thumb_path"] = thumbPath + else: + info["thumb_path"] = None + + if strip: + info.pop("rawlines",None) + info.pop("rawkeys",None) + info.pop("rawcommentkeys",None) + + return info + + def writeAssetFile(self, assetInfo, createBackup = True): + """ This (over)writes the asset file named in the assetInfo's "absolute path" key. If createBackup is set to True, any pre-existing file will be backed up to it's former name + ".bak" """ + if not assetInfo: + raise ValueError('Cannot use None as assetInfo') + + ap = assetInfo["absolute path"] + bak = ap + ".bak" + + if createBackup and os.path.isfile(ap): + shutil.copy(ap,bak) + + with open(ap, 'w', encoding='utf8') as f: + + stillNeedToDumpCommentKeys = True + + writtenKeys = [] + writtenCommentKeys = [] + writtenExtraKeys = [] + + remainingKeys = list(assetInfo["pertinentKeys"]) + remainingCommentKeys = list(assetInfo["pertinentCommentKeys"]) + remainingExtraKeys = list(assetInfo["pertinentExtraKeys"]) + + for line in assetInfo["rawlines"]: + allowWrite = True + m = re.match(r"^([a-zA-Z_]+)\s+(.*)$",line) + if m: + # If this is the first line without a hash sign, we want to + # dump the remaining comment keys before doing anything else + if stillNeedToDumpCommentKeys: + if len(remainingCommentKeys) > 0: + for key in remainingCommentKeys: + if not assetInfo[key] is None: + f.write("# " + key + " " + assetInfo[key] + "\x0a") + + stillNeedToDumpCommentKeys = False + + key = m.group(1) + + if key in remainingKeys: + allowWrite = False + if not assetInfo[key] is None: + f.write(key + " " + assetInfo[key] + "\x0a") + writtenKeys.append(key) + remainingKeys.remove(key) + + if key in remainingExtraKeys: + allowWrite = False + + if not assetInfo[key] is None and len(assetInfo[key]) > 0 and not key in writtenExtraKeys: + for val in assetInfo[key]: + f.write(key + " " + val + "\x0a") + writtenExtraKeys.append(key) + remainingExtraKeys.remove(key) + + if key in writtenExtraKeys: + allowWrite = False + + m = re.match(r"^#\s+([a-zA-Z_]+)\s+(.*)$",line) + if m: + key = m.group(1) + + if key in remainingCommentKeys: + allowWrite = False + if not assetInfo[key] is None: + f.write("# " + key + " " + assetInfo[key] + "\x0a") + writtenCommentKeys.append(key) + remainingCommentKeys.remove(key) + + if allowWrite: + f.write(line + "\x0a") + + if len(remainingKeys) > 0: + for key in remainingKeys: + if not assetInfo[key] is None: + f.write(key + " " + assetInfo[key] + "\x0a") + + if len(remainingExtraKeys) > 0: + for key in remainingExtraKeys: + if not assetInfo[key] is None and len(assetInfo[key]) > 0: + for val in assetInfo[key]: + f.write(key + " " + val + "\x0a") + + return True + + def materialToHash(self, material): + """Convert a material object to a hash containing all its settings""" + output = {} + + fn = os.path.abspath(material.filename) + + # meta + + output["name"] = material.name + output["description"] = material.description + output["materialFile"] = fn + + # colors + + output["ambientColor"] = material.ambientColor.values + output["diffuseColor"] = material.diffuseColor.values + output["specularColor"] = material.specularColor.values + output["emissiveColor"] = material.emissiveColor.values + + # textures + + output["diffuseTexture"] = material.diffuseTexture + output["bumpMapTexture"] = material.bumpMapTexture + output["normalMapTexture"] = material.normalMapTexture + output["displacementMapTexture"] = material.displacementMapTexture + output["specularMapTexture"] = material.specularMapTexture + output["transparencyMapTexture"] = material.transparencyMapTexture + output["aoMapTexture"] = material.aoMapTexture + + # texture intensities + + output["bumpMapIntensity"] = material.bumpMapIntensity + output["normalMapIntensity"] = material.normalMapIntensity + output["displacementMapIntensity"] = material.displacementMapIntensity + output["specularMapIntensity"] = material.specularMapIntensity + output["transparencyMapIntensity"] = material.transparencyMapIntensity + output["aoMapIntensity"] = material.aoMapIntensity + + # subsurface + + output["sssEnabled"] = material.sssEnabled + output["sssRScale"] = material.sssRScale + output["sssGScale"] = material.sssGScale + output["sssBScale"] = material.sssBScale + + # various + + output["uvMap"] = material.uvMap + output["shininess"] = material.shininess + output["opacity"] = material.opacity + output["translucency"] = material.translucency + output["shadeless"] = material.shadeless + output["wireframe"] = material.wireframe + output["transparent"] = material.transparent + output["alphaToCoverage"] = material.alphaToCoverage + output["backfaceCull"] = material.backfaceCull + output["depthless"] = material.depthless + output["castShadows"] = material.castShadows + output["receiveShadows"] = material.receiveShadows + output["autoBlendSkin"] = material.autoBlendSkin + + # viewport color + if material.usesViewPortColor(): + output["viewPortColor"] = material.viewPortColor.values + output["viewPortAlpha"] = material._viewPortAlpha + + for key in output.keys(): + if output[key] is None: + output[key] = "" + + definedKeys = [] + for key in output: + definedKeys.append(str(key).lower()) + + with open(fn, 'r', encoding='utf-8') as f: + line = f.readline() + while line: + parsedLine = line.strip() + if parsedLine and not parsedLine.startswith("#") and not parsedLine.startswith("/"): + match = re.search(r'^([a-zA-Z]+)\s+(.*)$', parsedLine) + if match: + key = match.group(1) + value = match.group(2) + if not key.lower() in definedKeys: + # There was a key defined in the material file, but it has not been picked up + # by MH. So we insert it in the produced hash and let the recipient decide + # what to do with it, if anything + output[key] = value + line = f.readline() + + return output + + def _findMaterials(self,path): + matches = [] + for root, dirnames, filenames in os.walk(path): + for filename in fnmatch.filter(filenames, '*.mhmat'): + matches.append(os.path.join(root, filename)) + return matches + + def _findProxies(self,path): + + basenames = [] + matches = [] + for root, dirnames, filenames in os.walk(path): + for filename in fnmatch.filter(filenames, '*.mhpxy'): + matches.append(os.path.join(root, filename)) + basenames.append(os.path.basename(filename)) + + for root, dirnames, filenames in os.walk(path): + for filename in fnmatch.filter(filenames, '*.mhclo'): + bn = os.path.basename(filename) + if not bn in basenames: + matches.append(os.path.join(root, filename)) + + return matches + + def getAvailableSystemSkins(self): + """Get a list with full paths to all system skins (the MHMAT files)""" + path = getpath.getSysDataPath("skins") + return self._findMaterials(path) + + def getAvailableUserSkins(self): + """Get a list with full paths to all user skins (the MHMAT files)""" + path = getpath.getDataPath("skins") + return self._findMaterials(path) + + def getAvailableSystemHair(self): + """Get a list with full paths to all system hair (the MHCLO files)""" + path = getpath.getSysDataPath("hair") + return self._findProxies(path) + + def getAvailableUserHair(self): + """Get a list with full paths to all user hair (the MHCLO files)""" + path = getpath.getDataPath("hair") + return self._findProxies(path) + + def getAvailableSystemEyebrows(self): + """Get a list with full paths to all system eyebrows (the MHCLO files)""" + path = getpath.getSysDataPath("eyebrows") + return self._findProxies(path) + + def getAvailableUserEyebrows(self): + """Get a list with full paths to all user eyebrows (the MHCLO files)""" + path = getpath.getDataPath("eyebrows") + return self._findProxies(path) + + def getAvailableSystemEyelashes(self): + """Get a list with full paths to all system eyelashes (the MHCLO files)""" + path = getpath.getSysDataPath("eyelashes") + return self._findProxies(path) + + def getAvailableUserEyelashes(self): + """Get a list with full paths to all user eyelashes (the MHCLO files)""" + path = getpath.getDataPath("eyelashes") + return self._findProxies(path) + + def getAvailableSystemClothes(self): + """Get a list with full paths to all system clothes (the MHCLO files)""" + path = getpath.getSysDataPath("clothes") + return self._findProxies(path) + + def getAvailableUserClothes(self): + """Get a list with full paths to all user clothes (the MHCLO files)""" + path = getpath.getDataPath("clothes") + return self._findProxies(path) + + def _equipProxy(self, category, tab, filename): + tv = self.api.ui.getTaskView(category, tab) + if tv is None: + raise ValueError("Could not find taskview " + str(category) + "/" + str(tab)) + tv.proxyFileSelected(filename) + + def _unequipProxy(self, category, tab, filename): + tv = self.api.ui.getTaskView(category, tab) + if tv is None: + raise ValueError("Could not find taskview " + str(category) + "/" + str(tab)) + tv.proxyFileDeselected(filename) + + def _getEquippedProxies(self, category, tab, onlyFirst=False): + tv = self.api.ui.getTaskView(category, tab) + if tv is None: + raise ValueError("Could not find taskview " + str(category) + "/" + str(tab)) + ps = tv.selectedProxies + + if onlyFirst: + if ps is None or len(ps) < 1: + return None + return ps[0].file + else: + ret = [] + if ps is None: + return [] + for p in ps: + ret.append(p.file) + return ret + + def equipHair(self, mhclofile): + """Equip a MHCLO file with hair. This will automatically unequip previously equipped hair.""" + self._equipProxy("Geometries","Hair",mhclofile) + + def unequipHair(self, mhclofile): + """Unequip a MHCLO file with hair""" + self._unequipProxy("Geometries", "Hair", mhclofile) + + def getEquippedHair(self): + """Get the currently equipped hair, if any""" + return self._getEquippedProxies("Geometries","Hair",onlyFirst=True) + + def equipEyebrows(self, mhclofile): + """Equip a MHCLO file with eyebrows. This will automatically unequip previously equipped eyebrows.""" + self._equipProxy("Geometries", "Eyebrows", mhclofile) + + def unequipEyebrows(self, mhclofile): + """Unequip a MHCLO file with eyebrows""" + self._unequipProxy("Geometries", "Eyebrows", mhclofile) + + def getEquippedEyebrows(self): + """Get the currently equipped eyebrows, if any""" + return self._getEquippedProxies("Geometries", "Eyebrows", onlyFirst=True) + + def equipEyelashes(self, mhclofile): + """Equip a MHCLO file with eyelashes. This will automatically unequip previously equipped eyelashes.""" + self._equipProxy("Geometries", "Eyelashes", mhclofile) + + def unequipEyelashes(self, mhclofile): + """Unequip a MHCLO file with eyelashes""" + self._unequipProxy("Geometries", "Eyelashes", mhclofile) + + def getEquippedEyelashes(self): + """Get the currently equipped eyelashes, if any""" + return self._getEquippedProxies("Geometries", "Eyelashes", onlyFirst=True) + + def equipClothes(self, mhclofile): + """Equip a MHCLO file with clothes""" + self._equipProxy("Geometries", "Clothes", mhclofile) + + def unequipClothes(self, mhclofile): + """Unequip a MHCLO file with clothes""" + self._unequipProxy("Geometries", "Clothes", mhclofile) + + def getEquippedClothes(self): + """Get a list of all currently equipped clothes""" + return self._getEquippedProxies("Geometries", "Clothes") + + def unequipAllClothes(self): + """Unequip all clothes""" + for c in self.getEquippedClothes(): + self.unequipClothes(c) diff --git a/makehuman/plugins/1_mhapi/_exports.py b/makehuman/plugins/1_mhapi/_exports.py new file mode 100644 index 000000000..97ea554a8 --- /dev/null +++ b/makehuman/plugins/1_mhapi/_exports.py @@ -0,0 +1,85 @@ +#!/usr/bin/python + +from .namespace import NameSpace + +import getpath +import os +import sys +import gui3d +import gui +import mh + +class Exports(NameSpace): + """This namespace wraps all calls that are related to producing file output.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + + def _getExportTaskView(self): + return self.api.ui.getTaskView("Files","Export") + + def getExportFormats(self): + tv = self._getExportTaskView() + return list(tv.formats) + + def getExporterByClassname(self, className): + className = className.lower() + for f in self.getExportFormats(): + cn = str(type(f[0])).lower() + if className in cn: + return f[0] + return None + + def _getDummyFileEntry(self, outputFilename, useExportsDir=True): + def fileentry(ext): + of = outputFilename + if useExportsDir: + of = os.path.basename(of) + ed = mh.getPath("exports") + of = os.path.join(ed,of) + return of + return fileentry + + def getOBJExporter(self): + return self.getExporterByClassname("ExporterOBJ") + + def getFBXExporter(self): + return self.getExporterByClassname("ExporterFBX") + + def getDAEExporter(self): + return self.getExporterByClassname("ExporterCollada") + + def getMHX2Exporter(self): + return self.getExporterByClassname("ExporterMHX2") + + def exportAsOBJ(self, outputFilename, useExportsDir=True): + """Export the current toon as wavefront obj.""" + e = self.getOBJExporter() + human = gui3d.app.selectedHuman + fileentry = self._getDummyFileEntry(outputFilename, useExportsDir) + e.export(human, fileentry) + + def exportAsFBX(self, outputFilename, useExportsDir=True): + """Export the current toon as wavefront obj.""" + e = self.getFBXExporter() + human = gui3d.app.selectedHuman + fileentry = self._getDummyFileEntry(outputFilename, useExportsDir) + e.export(human, fileentry) + + def exportAsDAE(self, outputFilename, useExportsDir=True): + """Export the current toon as wavefront obj.""" + e = self.getDAEExporter() + human = gui3d.app.selectedHuman + fileentry = self._getDummyFileEntry(outputFilename, useExportsDir) + e.export(human, fileentry) + + def exportAsMHX2(self, outputFilename, useExportsDir=True): + """Export the current toon as wavefront obj.""" + e = self.getMHX2Exporter() + human = gui3d.app.selectedHuman + fileentry = self._getDummyFileEntry(outputFilename, useExportsDir) + e.export(human, fileentry) + + diff --git a/makehuman/plugins/1_mhapi/_internals.py b/makehuman/plugins/1_mhapi/_internals.py new file mode 100644 index 000000000..8520a49ff --- /dev/null +++ b/makehuman/plugins/1_mhapi/_internals.py @@ -0,0 +1,47 @@ +#!/usr/bin/python + +from .namespace import NameSpace +from .JsonCall import JsonCall + +from core import G + +class Internals(NameSpace): + """The *internals* namespace hierarcy consists of a number of namespaces collecting calls for gaining low-level access to internal MakeHuman functionality. +The idea with these is that you *can* get access to such functionality if you need it, but most definitely not that you *should*. + +In the vast majority of cases, you would benefit from first trying to find a relevant call elsewhere in the API, and as a last resort look here.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.JsonCall = JsonCall + self.trace() + + def getHuman(self): + """Get the central human object.""" + self.trace() + return G.app.selectedHuman + + def getApp(self): + """Get the central app object.""" + self.trace() + return G.app + + def getSkeleton(self): + """Get the human's skeleton, if any.""" + self.trace() + return G.app.selectedHuman.getSkeleton() + + def numpyTypecodeToPythonTypeCode(self, numpyTypeCode): + """Get the python array type code that is closest to the given numpy type code.""" + if numpyTypeCode == "= (3,4): + import importlib + import importlib.util + from PyQt5 import QtGui + from PyQt5 import QtCore + from PyQt5.QtGui import * + from PyQt5 import QtWidgets + from PyQt5.QtWidgets import * +else: + import pkgutil + if pkgutil.find_loader("PySide") is not None: + from PySide import QtGui + from PySide import QtCore + from PySide.QtGui import * + else: + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import * + + +class ComboBox(QComboBox, QWidget): + + onChangeMethod = None + + def __init__(self,data = None, onChange = None): + super(ComboBox, self).__init__() + self.currentIndexChanged.connect(self._onChange) + if data: + self.setData(data) + if onChange: + self.onChangeMethod = onChange + + # Padding is because Qt has a bug that ignores style color in a + # combobox without it (for some incomprehensible reason) + self.setStyleSheet("QComboBox { color: white; padding: 2px }") + + def setData(self,items): + self.clear() + for item in items: + self.addItem(item) + + def getCurrentItem(self): + return self.currentText() + + def rowCount(self): + return len( [item for item in self.getItems() if not item.isHidden()] ) + + def setCurrentRow(self,row): + self.setCurrentIndex(row) + + def setCurrentItem(self,itemText): + index = self.findText(itemText, QtCore.Qt.MatchFixedString) + if index >= 0: + self.setCurrentIndex(index) + + def setOnChange(self,onChange): + self.onChangeMethod = onChange + + def _onChange(self): + #log.debug("onChange") + if self.onChangeMethod: + self.onChangeMethod(self.getCurrentItem()) + +class UI(NameSpace): + """This namespace wraps all calls that are related to working with the user interface.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + self.QtCore = QtCore + self.QtGui = QtGui + + def getAllCategories(self): + categories = [] + for catName in G.app.categories.keys(): + categories.append(G.app.categories[catName]) + return categories + + def getAllTaskViews(self): + taskviews = [] + for category in self.getAllCategories(): + taskNames = category.tasksByName.keys() + for taskName in taskNames: + taskView = category.tasksByName[taskName] + taskviews.append(taskView) + return taskviews + + def getTaskView(self, categoryName, taskName): + category = G.app.categories[categoryName] + return category.tasksByName[taskName] + + def createWidget(self): + return gui.Widget() + + def createTab(self,parent,name,label): + return gui.Tab(parent,name,label) + + def createGroupBox(self,label): + return gui.GroupBox(label) + + def createSlider(self, value=0.0, min=0.0, max=1.0, label=None, vertical=False, valueConverter=None, image=None, scale=1000): + return gui.Slider(value,min,max,label,vertical,valueConverter,image,scale) + + def createButton(self, label=None, selected=False): + return gui.Button(label,selected) + + def createCheckBox(self, label=None, selected=False): + return gui.CheckBox(label,selected) + + def createComboBox(self, data = None, onChange = None): + return ComboBox(data, onChange) + + def createList(self, data=None): + l = gui.ListView() + if data: + l.setData(data) + return l + + def createLabel(self, label=''): + return gui.TextView(label) + + def createTextEdit(self, text='', validator = None): + return gui.TextEdit(text,validator) + + + + + diff --git a/makehuman/plugins/1_mhapi/_utility.py b/makehuman/plugins/1_mhapi/_utility.py new file mode 100644 index 000000000..98701ccc4 --- /dev/null +++ b/makehuman/plugins/1_mhapi/_utility.py @@ -0,0 +1,245 @@ +#!/usr/bin/python + +from .namespace import NameSpace +import sys +import os +import io +import struct +import inspect +import array + +from .logchannel import LogChannel + +class Utility(NameSpace): + """This namespace wraps various calls which are convenient but not necessarily MH-specific.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + + self.isPy3 = (sys.version_info >= (3,0)) + self.debugWriter = {} + + if self.isPython3(): + import urllib.request + self.urlrequest = urllib.request + import importlib + import importlib.util + self.hasPySide = (importlib.util.find_spec("PySide") is not None) + self.hasPyQt = (importlib.util.find_spec("PyQt4") is not None) + else: + import urllib2 + self.urlrequest = urllib2 + import pkgutil + self.hasPySide = (pkgutil.find_loader("PySide") is not None) + self.hasPyQt = (pkgutil.find_loader("PyQt4") is not None) + + self.logChannels = {} + + def getTypeAsString(self, content): + + if isinstance(content, int): + return "int" + if isinstance(content, float): + return "float" + if isinstance(content, dict): + return "dict" + if isinstance(content, list): + return "list" + if isinstance(content, array.array): + return "array(" + content.typecode + ")" + if self.isPy3: + if isinstance(content, bytes): + return "bytes" + if isinstance(content, str): + return "unicode" + else: + if isinstance(content, unicode): + return "unicode" + if isinstance(content, str): + return "bytes" + + # None of the listed types matched + + with open("/tmp/missingtype.txt","a", encoding='utf-8') as f: + f.write(str(type(content))) + f.write("\n") + + return str(type(content)) + + def getValueAsString(self, content, newLinesIfComplex=False): + if isinstance(content, list) or isinstance(content, array.array): + result = "[]" + #for value in content: + # if result != "": + # if newLinesIfComplex: + # result = result + ",\n" + # else: + # result = result + ", " + # result = result + self.getValueAsString(value) + return result + if self.isPy3: + if isinstance(content, str): + return content + if isinstance(content, bytes): + try: + val = content.decode("utf-8") + except: + import binascii + val = binascii.hexlify(content).decode("utf-8") + return val + else: + if isinstance(content, str): + return content + if isinstance(content, float): + # This is to parry differences in precision in py2 vs py3 + # Even when using round(), results will differ. Thus we do + # a destructive floor instead. + precision = 100000.0 + intContent = int(precision * round(content,8)) + floatContent = float(intContent) / precision + return "{0:.4f}".format(floatContent) + return str(content) + + def isPySideAvailable(self): + return self.hasPySide + + def isPyQtAvailable(self): + return self.hasPyQt + + def isPython3(self): + return self.isPy3 + + def getCompatibleUrlFetcher(self): + return self.urlrequest + + def getLogChannel(self, name, defaultLevel=2, mirrorToMHLog=False): + if name not in self.logChannels: + self.logChannels[name] = LogChannel(name,defaultLevel,mirrorToMHLog) + return self.logChannels[name] + + def resetDebugWriter(self, channelName = "unsorted"): + self.debugWriter[channelName] = 0 + debugDir = self.api.locations.getUserHomePath("debugWriter") + subPath = os.path.join( os.path.abspath(debugDir), channelName ) + if not os.path.exists(subPath): + os.makedirs(subPath) + fnTxt = os.path.join(subPath, "textualContent.txt") + if os.path.exists(fnTxt): + os.remove(fnTxt) + fnTxt = os.path.join(subPath, "debugContent.txt") + if os.path.exists(fnTxt): + os.remove(fnTxt) + + def _py3debugWrite(self, content, fnBin, fnTxt): + with open(fnBin, "wb") as f: + wasWritten = False + if isinstance(content, list): + for value in content: + if isinstance(value, str): + f.write( bytes(value, 'utf-8') ) + else: + if isinstance(value, float): + f.write( struct.pack("f", value) ) + else: + f.write( bytes(value) ) + wasWritten = True + if isinstance(content, str) and not wasWritten: + f.write( bytes(content, 'utf-8') ) + wasWritten = True + if not wasWritten: + if isinstance(content, int): + f.write( struct.pack(" 1: + (frame, filename, line_number, function_name, lines, index) = stack[i] + fn = os.path.basename(filename) + if not fn in exclude: + f.write(fn + " -> " + function_name) + f.write("\n") + i = i - 1 + + f.write("\n") + + with open(fnStack, "w", encoding='utf-8') as f: + f.write(str(increment)) + f.write("\n") + + stack = inspect.stack() + + i = len(stack) - 1 + + exclude = ["makehuman.py", "qtui.py", "qtgui.py", "mhmain.py"] + + while i > 1: + (frame, filename, line_number, function_name, lines, index) = stack[i] + fn = os.path.basename(filename) + fn = fn.replace(".py", "") + if not fn in exclude: + f.write(fn + "." + function_name + "():" + str(line_number)) + f.write("\n") + i = i - 1 + f.write("\n") + + diff --git a/makehuman/plugins/1_mhapi/_version.py b/makehuman/plugins/1_mhapi/_version.py new file mode 100644 index 000000000..6380b688e --- /dev/null +++ b/makehuman/plugins/1_mhapi/_version.py @@ -0,0 +1,68 @@ +#!/usr/bin/python + +from .namespace import NameSpace +import makehuman + +class Version(NameSpace): + """This namespace wraps all calls that are related to hg and MH version.""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + + self.rev = None + self.revid = None + self.branch = None + + try: + hg = makehuman.get_revision_hg_info() + if hg: + self.rev = hg[0] + self.revid = hg[1] + self.branch = hg[2] + except: + pass + + def getBranch(self): + """Returns the name of the current local code branch, for example 'default'. If this is not possible to deduce, None is returned.""" + self.trace() + return self.branch + + def getRevision(self): + """Return the full textual representation of the Hg revision, for example 'r1604 (d48f36771cc0)'. If this is not possible to deduce, None is returned.""" + self.trace() + if not self.rev: + return None + + if not self.revid: + return None + + return self.rev + " (" + self.revid + ")" + + def getRevisionId(self): + """Return the hash id of the Hg revision, for example 'd48f36771cc0'. If this is not possible to deduce, None is returned.""" + self.trace() + return self.revid + + def getRevisionNumber(self): + """Returns the number of the current local revision as an integer, for example 1604. If this is not possible to deduce, None is returned.""" + self.trace() + return self.rev + + def getFullVersion(self): + """Returns the full textual description of the current version, for example 'MakeHuman unstable 20141120' or 'MakeHuman 1.0.2'.""" + self.trace() + return makehuman.getVersionStr(True,True) + + def getVersionNumberAsArray(self): + """Returns the numeric representation of the version number as cells in an array, for example [1, 0, 2].""" + self.trace() + return makehuman.getVersion() + + def getVersionNumberAsString(self): + """Returns the string representation of the version number, for example '1.0.2'.""" + self.trace() + return makehuman.getVersionDigitsStr() + + diff --git a/makehuman/plugins/1_mhapi/_viewport.py b/makehuman/plugins/1_mhapi/_viewport.py new file mode 100644 index 000000000..2982dcb2f --- /dev/null +++ b/makehuman/plugins/1_mhapi/_viewport.py @@ -0,0 +1,12 @@ +#!/usr/bin/python + +from .namespace import NameSpace + +class Viewport(NameSpace): + """This namespace wraps calls which relate to the viewport (camera position etc).""" + + def __init__(self,api): + self.api = api + NameSpace.__init__(self) + self.trace() + diff --git a/makehuman/plugins/1_mhapi/api.py b/makehuman/plugins/1_mhapi/api.py new file mode 100644 index 000000000..b0a932148 --- /dev/null +++ b/makehuman/plugins/1_mhapi/api.py @@ -0,0 +1,44 @@ +#!/usr/bin/python + +from .namespace import NameSpace + +class API(NameSpace): + + def __init__(self,app): + self._app = app + NameSpace.__init__(self) + self.trace() + + from ._assets import Assets + self.assets = Assets(self) + + from ._exports import Exports + self.exports = Exports(self) + + from ._internals import Internals + self.internals = Internals(self) + + from ._mesh import Mesh + self.mesh = Mesh(self) + + from ._locations import Locations + self.locations = Locations(self) + + from ._version import Version + self.version = Version(self) + + from ._viewport import Viewport + self.viewport = Viewport(self) + + from ._modifiers import Modifiers + self.modifiers = Modifiers(self) + + from ._ui import UI + self.ui = UI(self) + + from ._utility import Utility + self.utility = Utility(self) + + from ._skeleton import Skeleton + self.skeleton = Skeleton(self) + diff --git a/makehuman/plugins/1_mhapi/logchannel.py b/makehuman/plugins/1_mhapi/logchannel.py new file mode 100644 index 000000000..02cd61391 --- /dev/null +++ b/makehuman/plugins/1_mhapi/logchannel.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 + +import log +import os +import gui3d +import inspect +import getpath + +class LogChannel(): + + CRASH = 0 + ERROR = 1 + WARN = 2 + INFO = 3 + DEBUG = 4 + TRACE = 5 + SPAM = 6 + + _levels = ["CRASH", "ERROR", "WARN ", "INFO ", "DEBUG", "TRACE", "SPAM"] + + def __init__(self, name, defaultLevel = 2, mirrorToMHLog = False): + + self.name = name + + if name in os.environ and os.environ.get(name,"").isdigit(): + defaultLevel = int(os.environ.get(name,"2")) + + if "mirrorToMHLog" in os.environ and os.environ.get("mirrorToMHLog","") != "": + mirrorToMHLog = True + + self.level = defaultLevel + self.mirror = mirrorToMHLog + self.api = gui3d.app.mhapi + + self.root = self.api.locations.getUserHomePath("plugin_logs") + + if not os.path.exists(self.root): + os.makedirs(self.root) + + self.fileName = os.path.join(self.root,name + ".txt") + + fnDecoded = getpath.pathToUnicode(self.fileName) + with open(self.fileName,"wt", encoding='utf-8') as f: + f.write("--- " + fnDecoded + " ---\n\n") + + def _logItem(self,level,message,item): + if level > self.level: + return + + stack = inspect.stack() + (frame, filename, line_number, function_name, lines, index) = stack[2] + + loc = "{}/{}():{}".format(os.path.basename(filename),function_name, line_number) + + leveln = self._levels[level] + outStr = "" + if item is not None: + outstr = "[{0}] {1} {2} {3}".format(leveln,loc,message,item) + else: + outstr = "[{0}] {1} {2}".format(leveln,loc,message) + + if self.mirror: + log.debug(outstr) + + with open(self.fileName, "at",encoding='utf-8') as f: + f.write(outstr + "\n") + + def crash(self, message, item = None): + self._logItem(self.CRASH,message,item) + + def error(self, message, item = None): + self._logItem(self.ERROR,message,item) + + def warn(self, message, item = None): + self._logItem(self.WARN,message,item) + + def info(self, message, item = None): + self._logItem(self.INFO,message,item) + + def debug(self, message, item = None): + self._logItem(self.DEBUG,message,item) + + def trace(self, message, item = None): + self._logItem(self.TRACE,message,item) + + def spam(self, message, item = None): + self._logItem(self.SPAM,message,item) diff --git a/makehuman/plugins/1_mhapi/namespace.py b/makehuman/plugins/1_mhapi/namespace.py new file mode 100644 index 000000000..aaf965873 --- /dev/null +++ b/makehuman/plugins/1_mhapi/namespace.py @@ -0,0 +1,73 @@ +#!/usr/bin/python + +import inspect +import sys +from abc import * + +# When developing the API we want to get a continuous output of which +# methods are called and within which namespace. Setting this to true +# will print all such accesses to the console prompt +api_tracing = False + +# The following is an ugly copy/paste, but since python2 and python3 +# have different syntaxes regarding abstract base classes, this is +# the most stable solution + +if sys.version_info >= (3,0): + + class NameSpace(ABC): + + def __init__(self): + global api_tracing + self.tracing = api_tracing + self.trace() + + # Utility method for printing info about where we are in the code + # to the console prompt. In the future we should probably use the + # log function instead. + def trace(self): + if self.tracing: + info = dict() + + stack = inspect.currentframe().f_back + info["line_number"] = str(stack.f_lineno) + info["caller_name"] = stack.f_globals["__name__"] + info["file_name"] = stack.f_globals["__file__"] + info["caller_method"] = inspect.stack()[1][3] + + stack = inspect.stack() + info["caller_class"] = str(stack[1][0].f_locals["self"].__class__) + + print("TRACE {}.{}():{}".format(info["caller_name"], info["caller_method"], info["line_number"])) + +else: + + class NameSpace: + + __metaclass__ = ABCMeta + + def __init__(self): + global api_tracing + self.tracing = api_tracing + self.trace() + + # Utility method for printing info about where we are in the code + # to the console prompt. In the future we should probably use the + # log function instead. + def trace(self): + if self.tracing: + info = dict() + + stack = inspect.currentframe().f_back + info["line_number"] = str(stack.f_lineno) + info["caller_name"] = stack.f_globals["__name__"] + info["file_name"] = stack.f_globals["__file__"] + info["caller_method"] = inspect.stack()[1][3] + + stack = inspect.stack() + info["caller_class"] = str(stack[1][0].f_locals["self"].__class__) + + print("TRACE {}.{}():{}".format(info["caller_name"], info["caller_method"], info["line_number"])) + + + diff --git a/makehuman/plugins/1_mhapi/testjson.py b/makehuman/plugins/1_mhapi/testjson.py new file mode 100644 index 000000000..b362c5f25 --- /dev/null +++ b/makehuman/plugins/1_mhapi/testjson.py @@ -0,0 +1,33 @@ +#!/usr/bin/python + +from .JsonCall import JsonCall + +a = dict() +a["aaa"] = 1; +a["bbb"] = None +a["ccc"] = ["a","b",1.54] + +jsc = JsonCall() +jsc.setFunction("testfunction") +jsc.setParam("hej","hopp") +jsc.setParam("abc",2) +jsc.setData([ [1,2,3.3], [1,2,3], a ]) + +print(jsc.serialize()) + +#js = "{\ +# \"function\": \"testfunction\",\ +# \"error\": \"\",\ +# \"params\": {\ +# \"abc\": 2,\ +# \"hej\": \"hopp\"\ +# },\ +# \"data\": null\ +#}" +# +#jsc = JsonCall(js) +# +#print jsc.serialize() + + + diff --git a/makehuman/plugins/8_asset_downloader/__init__.py b/makehuman/plugins/8_asset_downloader/__init__.py new file mode 100644 index 000000000..63fb27ee7 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/__init__.py @@ -0,0 +1,42 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** TBD + +**Code Home Page:** TBD + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import log +import json + +from .assetdownload import AssetDownloadTaskView + +category = None + +downloadView = None + +def load(app): + category = app.getCategory('Community') + downloadView = category.addTask(AssetDownloadTaskView(category)) + +def unload(app): + pass + diff --git a/makehuman/plugins/8_asset_downloader/assetcleaner.py b/makehuman/plugins/8_asset_downloader/assetcleaner.py new file mode 100644 index 000000000..d5f379ac8 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/assetcleaner.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** http://www.makehumancommunity.org + +**Code Home Page:** https://github.com/makehumancommunity/community-plugins + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import json +import os +import re +import platform +import calendar, datetime + +from progress import Progress + +from core import G + +mhapi = gui3d.app.mhapi + +class AssetCleaner(): + + def __init__(self, remoteAsset): + + self.log = mhapi.utility.getLogChannel("assetdownload") + + self.asset = remoteAsset + self.assetType = remoteAsset.getType() + + self._mhmat_as_string = "" + + for fn in remoteAsset.localFiles.keys(): + ext = os.path.splitext(fn) + self.log.debug("ext",ext) + + def checkForMissingFiles(self): + return [] + + def _getTextureTuples(self): + + if self._mhmat_as_string is None or self._mhmat_as_string == "": + return [] + + return [] + + def _cleanMHMAT(self): + textures = self._getTextureTuples() + pass + + def _cleanMHCLO(self): + pass + + def _fixClothes(self): + pass + + def _fixMaterial(self): + pass + + def cleanAsset(self): + + if self.assetType in ["eyebrows","eyelashes","teeth","hair","clothes"]: + self._fixClothes() + + if self.assetType == "material": + self._fixMaterial() + + diff --git a/makehuman/plugins/8_asset_downloader/assetdb.py b/makehuman/plugins/8_asset_downloader/assetdb.py new file mode 100644 index 000000000..ef4e840f0 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/assetdb.py @@ -0,0 +1,481 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** http://www.makehumancommunity.org + +**Code Home Page:** https://github.com/makehumancommunity/community-plugins + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import json +import os +import re +import platform +import calendar +import sys +import datetime +import shutil + +from zipfile import ZipFile +from progress import Progress + +from core import G + +from .remoteasset import RemoteAsset +from .downloadtask import DownloadTask + +mhapi = gui3d.app.mhapi + +class AssetDB(): + + def __init__(self, parent): + + self.knownClothesCategories = [] + self.knownAuthors = [] + self.assetsById = dict() + self.isSynchronized = False + self.parent = parent + self.root = mhapi.locations.getUserDataPath("community-assets") + self.remotecache = os.path.join(self.root,"remotecache") + self.remotedb = os.path.join(self.root,"remote.json") + self.thumbseed = os.path.join(self.root, "asset-db-thumbnails.zip") + self.screenseed = os.path.join(self.root, "asset-db-screenshots.zip") + self.localdb = os.path.join(self.root,"local.json") + self.log = mhapi.utility.getLogChannel("assetdownload") + + self.localJson = None + + self.localAssets = {} + + self._loadRemoteDB() + + if os.path.exists(self.localdb): + self._loadLocalDB() + else: + if self.isSynchronized: + self._rebuildLocalDB() + self._loadLocalDB() + + def _loadRemoteDB(self): + self.log.trace("Enter") + + self.remoteAssets = {} + for key in mhapi.assets.getAssetTypes(): + if key != "node_setups_and_blender_specific": + self.remoteAssets[key] = {} + + self.log.debug("Remote json local path", self.remotedb) + + if not os.path.exists(self.remotedb): + self.log.warn("Remote json does not exist locally") + return + + with open(self.remotedb,"r") as f: + if mhapi.utility.isPython3(): + self.remoteJson = json.load(f) + else: + self.remoteJson = json.load(f,"UTF-8") + + self.log.spam("remoteJson",self.remoteJson) + + for assetId in self.remoteJson.keys(): + rawAsset = self.remoteJson[assetId] + asset = RemoteAsset(self,rawAsset,assetdb=self) + assetType = asset.getType() + + self.log.trace("assetId",assetId) + self.log.trace("assetType", assetType) + + self.log.spam("rawAsset",rawAsset) + if assetType == "clothes": + cat = asset.getCategory() + if cat not in self.knownClothesCategories: + self.knownClothesCategories.append(cat) + + self.knownClothesCategories.sort() + + if assetType not in self.remoteAssets and assetType != "node_setups_and_blender_specific": + self.log.error("Asset type not known:",assetType) + raise ValueError("Asset type not known: " + assetType) + else: + if assetType != "node_setups_and_blender_specific": + assetId = asset.getId() + self.remoteAssets[assetType][assetId] = asset + self.assetsById[assetId] = asset + + for assetType in self.remoteAssets: + for assetId in self.remoteAssets[assetType]: + author = self.remoteAssets[assetType][assetId].getAuthor() + if author not in self.knownAuthors: + self.knownAuthors.append(author) + + self.knownAuthors.sort() + + self.isSynchronized = True + + def _loadLocalDB(self): + self.log.trace("Enter") + + self.log.debug("About to load local json from", self.localdb) + + if not os.path.exists(self.localdb): + self.log.warn("Local json does not exist") + + self.localAssets = {} + for key in mhapi.assets.getAssetTypes(): + self.localAssets[key] = {} + + return + + with open(self.localdb, "r") as f: + if mhapi.utility.isPython3(): + self.localAssets = json.load(f) + else: + self.localAssets = json.load(f, "UTF-8") + + self.log.spam("localAssets",self.localAssets) + + def _rebuildLocalDB(self): + self.log.trace("Enter") + + self.log.debug("About to rebuild local DB") + self.localAssets = {} + for key in mhapi.assets.getAssetTypes(): + if key != "node_setups_and_blender_specific": + self.localAssets[key] = {} + + for assetType in self.remoteAssets.keys(): + for assetId in self.remoteAssets[assetType].keys(): + asset = self.remoteAssets[assetType][assetId] + self.log.spam("asset",asset) + location = asset.getInstallPath() + self.log.trace("asset location", location) + if os.path.exists(location): + fn = asset.getPertinentFileName() + self.log.trace("Pertinent file name", fn) + if fn is not None: + fn = os.path.join(location,fn) + if os.path.exists(fn): + self.log.trace("Installed asset location", location) + self.localAssets[assetType][assetId] = {} + self.localAssets[assetType][assetId]["file"] = fn + mod = os.path.getmtime(fn) + dt = datetime.datetime.fromtimestamp(mod) + self.localAssets[assetType][assetId]["modified"] = dt.strftime('%Y-%m-%d %H:%M:%S') + else: + self.log.trace("NOT installed asset", location) + else: + self.log.trace("NOT installed asset", location) + + self.log.spam("Local assets", self.localAssets) + + self._writeLocalDB() + + self.log.debug("Finished rebuilding local DB") + + def _writeLocalDB(self): + with open(self.localdb,"wt") as f: + json.dump(self.localAssets, f, indent=2) + + def getFilteredAssets(self, assetType, author=None, subtype=None, hasScreenshot=None, hasThumb=None, isDownloaded=None, title=None, desc=None, changed=None, license=None): + + outData = [] + + self.log.debug("Requesting filter with limits", { "assetType": assetType, "author": author, "subtype": subtype}) + + afterDate = None + + if changed is not None: + days = 36500 + + if changed.lower() == "one week": + days = 7 + + if changed.lower() == "one month": + days = 30 + + if changed.lower() == "three months": + days = 90 + + if changed.lower() == "one year": + days = 365 + + dt = datetime.datetime.now() + dt = dt - datetime.timedelta(days) + afterDate = dt.strftime('%Y-%m-%d %H:%M:%S') + + self.log.debug("afterDate",afterDate) + + if assetType in self.remoteAssets: + allData = self.remoteAssets[assetType] + for assetId in allData.keys(): + + asset = allData[assetId] + exclude = False + + if assetType == "clothes" and subtype is not None: + if subtype != asset.getCategory(): + exclude = True + + if assetType == "material" and subtype is not None: + btm = asset.belongs_to_metadata + if not btm["belonging_is_assigned"] or not "belongs_to_core_asset" in btm: + exclude = True + + if author is not None: + if author != asset.getAuthor(): + exclude = True + + if title is not None: + lt = title.lower() + if not lt in asset.getTitle().lower(): + exclude = True + + if desc is not None: + lt = desc.lower() + if not lt in asset.getDescription().lower(): + exclude = True + + if license is not None: + if license != asset.getLicense(): + exclude = True + + if isDownloaded is not None: + self.log.trace("isDownloaded",isDownloaded) + if isDownloaded == "yes": + self.log.trace("assetId",assetId) + self.log.trace("assetType",assetType) + if str(assetId) not in self.localAssets[assetType]: + exclude = True + else: + if str(assetId) in self.localAssets[assetType]: + exclude = True + + if afterDate is not None: + assetChanged = asset.getChanged() + self.log.trace("assetChanged",assetChanged) + if assetChanged != "" and assetChanged is not None: + if assetChanged < afterDate: + exclude = True + + if not exclude: + outData.append(asset) + + return outData + + def getDownloadTuples(self, ignoreExisting=True, onlyMeta=False, excludeThumb=False, excludeScreenshot=False): + allData = [] + for assetType in self.remoteAssets.keys(): + for assetId in self.remoteAssets[assetType].keys(): + asset = self.remoteAssets[assetType][assetId] + tuples = asset.getDownloadTuples(ignoreExisting, onlyMeta, excludeThumb, excludeScreenshot) + allData.extend(tuples) + + return allData + + def getKnownAuthors(self): + return list(self.knownAuthors) + + def getKnownClothesCategories(self): + return list(self.knownClothesCategories) + + def synchronizeRemote(self, parentWidget, onFinished=None, onProgress=None, downloadScreenshots=True, downloadThumbnails=True): + self.log.trace("Enter") + filesToDownload = [] + + self.downloadScreenshots = downloadScreenshots + self.downloadThumbnails = downloadThumbnails + + self.overrideProgressLength = 2 + self.overrideProgressAfterDownloads = 1 + + if not os.path.exists(self.root): + if downloadThumbnails: + filesToDownload.append(["http://download.tuxfamily.org/makehuman/assets/asset-db-thumbnails.zip",self.thumbseed]) + self.overrideProgressLength = self.overrideProgressLength + 2 + self.overrideProgressAfterDownloads = self.overrideProgressAfterDownloads + 1 + if downloadScreenshots: + filesToDownload.append(["http://download.tuxfamily.org/makehuman/assets/asset-db-screenshots.zip",self.screenseed]) + self.overrideProgressLength = self.overrideProgressLength + 2 + self.overrideProgressAfterDownloads = self.overrideProgressAfterDownloads + 1 + filesToDownload.append(["http://www.makehumancommunity.org/sites/default/files/assets.json", self.remotedb + ".keep"]) + else: + filesToDownload.append(["http://www.makehumancommunity.org/sites/default/files/assets.json",self.remotedb]) + + self.log.debug("overrideProgressLength",self.overrideProgressLength) + self.log.debug("overrideProgressAfterDownloads", self.overrideProgressAfterDownloads) + self.log.debug("filesToDownload",filesToDownload) + + self._syncParentWidget = parentWidget + self._synconFinished = onFinished + self._synconProgress = onProgress + + self._downloadTask = DownloadTask(parentWidget,filesToDownload,self._syncRemote1Finished,self._syncRemote1Progress) + + def _syncRemote1Progress(self,prog = 0.0): + self.log.trace("Enter") + + def _syncRemote1Finished(self, code=0, file=None): + self.log.trace("Enter") + + progress = Progress() + current = self.overrideProgressAfterDownloads + prog = float(current) / float(self.overrideProgressLength) + progress(prog, desc="Unzipping seed zips") + + if os.path.exists(self.thumbseed): + self.log.debug("HAS THUMB ZIP",self.thumbseed) + zip = ZipFile(self.thumbseed,'r') + if not os.path.exists(self.root): + os.makedirs(self.root) + zip.extractall(self.root) + zip.close() + if os.path.exists(self.remotedb): + os.remove(self.remotedb) + os.remove(self.thumbseed) + current = current + 1 + prog = float(current) / float(self.overrideProgressLength) + progress(prog, desc="Unzipping seed zips") + else: + self.log.debug("Did not have thumb seed zip") + + if os.path.exists(self.screenseed): + self.log.debug("HAS SCREEN ZIP",self.screenseed) + zip = ZipFile(self.screenseed,'r') + if not os.path.exists(self.root): + os.makedirs(self.root) + zip.extractall(self.root) + zip.close() + if os.path.exists(self.remotedb): + os.remove(self.remotedb) + os.remove(self.screenseed) + else: + self.log.debug("Did not have screen seed zip") + + current = current + 1 + prog = float(current) / float(self.overrideProgressLength) + progress(prog, desc="Checking for additional files to download") + + self.overrideProgressLength = None + self.overrideProgressAfterDownloads = None + + if os.path.exists(self.remotedb + ".keep"): + os.rename(self.remotedb + ".keep", self.remotedb) + + self._loadRemoteDB() + + filesToDownload = [] + + self.log.debug("downloadScreenshots",self.downloadScreenshots) + self.log.debug("downloadThumbnails",self.downloadThumbnails) + + for assetType in self.remoteAssets.keys(): + for assetId in self.remoteAssets[assetType].keys(): + remoteAsset = self.remoteAssets[assetType][assetId] + tuples = remoteAsset.getDownloadTuples(ignoreExisting=True,onlyMeta=True,excludeScreenshot=not self.downloadScreenshots,excludeThumb=not self.downloadThumbnails) + self.log.spam("Tuples",tuples) + filesToDownload.extend(tuples) + + self.log.debug("filesToDownload",filesToDownload) + + progress(1.0) + + self._downloadTask = DownloadTask(self._syncParentWidget,filesToDownload,self._syncRemote2Finished,self._syncRemote2Progress) + + + def _syncRemote2Progress(self,prog = 0.0): + self.log.trace("Enter") + + def _syncRemote2Finished(self, code=0, file=None): + self.log.trace("Enter") + + progress = Progress() + progress(0.1, desc="Rebuilding local asset DB") + self._rebuildLocalDB() + progress(0.5, desc="Loading local asset DB") + self._loadLocalDB() + progress(1.0) + + if self._synconFinished is not None: + self._synconFinished() + + def downloadItem(self, parentWidget, remoteAsset, onFinished=None, onProgress=None): + self.log.trace("Enter") + + filesToDownload = remoteAsset.getDownloadTuples(ignoreExisting=False,onlyMeta=False,excludeScreenshot=True,excludeThumb=False) + + self._downloadAsset = remoteAsset + self._downloadParentWidget = parentWidget + self._downloadonFinished = onFinished + self._downloadonProgress = onProgress + + self.log.spam("filesToDownload",filesToDownload) + + self._downloadTask = DownloadTask(parentWidget, filesToDownload, self._downloadFinished, self._downloadProgress) + + def _downloadFinished(self, code, file): + + if code > 0: + if self._downloadonFinished is not None: + self._downloadonFinished(code, file) + return + + remoteAsset = self._downloadAsset + + fn = remoteAsset.getPertinentFileName() + srcThumb = remoteAsset.getThumbPath() + + bn = os.path.basename(fn) + dn = remoteAsset.getInstallPath() + + (name,ext) = os.path.splitext(bn) + + self.log.debug("srcThumb",srcThumb) + self.log.debug("destThumb", os.path.join(dn,name + ".thumb")) + + if srcThumb: + destThumb = os.path.join(dn,name + ".thumb") + shutil.copyfile(srcThumb,destThumb) + + assetType = remoteAsset.getType() + assetId = remoteAsset.getId() + + file = os.path.join(dn,fn) + + self.log.debug("Downloaded file should be",file) + self.log.debug("assetId",assetId) + + if not os.path.exists(file): + self.log.error("File does not exist post download", file) + + mod = os.path.getmtime(file) + dt = datetime.datetime.fromtimestamp(mod) + modified = dt.strftime('%Y-%m-%d %H:%M:%S') + + self.localAssets[assetType][assetId] = { "file": file, "modified": modified } + + self._writeLocalDB() + + if self._downloadonFinished is not None: + self._downloadonFinished() + + def _downloadProgress(self, prog): + if self._downloadonProgress is not None: + self._downloadonProgress(prog) diff --git a/makehuman/plugins/8_asset_downloader/assetdownload.py b/makehuman/plugins/8_asset_downloader/assetdownload.py new file mode 100644 index 000000000..ab5bb1824 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/assetdownload.py @@ -0,0 +1,544 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** http://www.makehumancommunity.org + +**Code Home Page:** https://github.com/makehumancommunity/community-plugins + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import log +import json +import os +import re +import platform +import calendar, datetime + +from progress import Progress + +from core import G + +mhapi = gui3d.app.mhapi + +if mhapi.utility.isPython3(): + from PyQt5 import QtGui + from PyQt5 import QtCore + from PyQt5.QtGui import * + from PyQt5 import QtWidgets + from PyQt5.QtWidgets import * + from PyQt5.QtCore import * +else: + if mhapi.utility.isPySideAvailable(): + from PySide import QtGui + from PySide import QtCore + from PySide.QtGui import * + from PySide.QtCore import * + QtWidgets = PySide.Qt.QtWidgets + else: + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +from .assetdb import AssetDB +from .tablemodel import AssetTableModel +from .downloadtask import DownloadTask + +class AssetDownloadTaskView(gui3d.TaskView): + + def __init__(self, category): + gui3d.TaskView.__init__(self, category, 'Download assets') + + self.log = mhapi.utility.getLogChannel("assetdownload") + + self.notfound = mhapi.locations.getSystemDataPath("notfound.thumb") + self.assetdb = AssetDB(self) + + self._setupFilterBox() + self._setupSelectedBox() + self._setupSyncBox() + self._setupTable() + self._setupDetails() + + self.currentlySelectedRemoteAsset = None + self.isShowingDetails = False + + def onShow(self, event): + + if not os.path.exists(self.assetdb.root): + msg = "It seem that the asset database has not been downloaded yet. The asset database is needed in order to search for assets.\n\n" + msg = msg + "Downloading the database for the first time can take a long time on a slow connection, and it is normal that it occasionally " + msg = msg + "looks as if the download has stalled. Updating the database after it has been downloaded will be significantly faster.\n\n" + msg = msg + "After closing this dialog, click 'synchronize' in order to start downloading the asset database." + self.showMessage(msg) + + def _setupFilterBox(self): + self.log.trace("Enter") + self.filterBox = mhapi.ui.createGroupBox("Filter assets") + + self.types = [ + "pose", + "clothes", + "target", + "hair", + "teeth", + "eyebrows", + "eyelashes", + "skin", + "proxy", + "material", + "model", + "rig", + "expression" + ] + + self.filterBox.addWidget(mhapi.ui.createLabel("\nAsset type")) + self.cbxTypes = mhapi.ui.createComboBox(self.types, self._onTypeChange) + self.filterBox.addWidget(self.cbxTypes) + + self.filterBox.addWidget(mhapi.ui.createLabel("\nAsset subtype")) + self.cbxSubTypes = mhapi.ui.createComboBox(["-- any --"]) + self.filterBox.addWidget(self.cbxSubTypes) + + self.authors = ["-- any --"] + self.authors.extend(sorted(self.assetdb.getKnownAuthors(), key=lambda s: s.lower())) + + self.filterBox.addWidget(mhapi.ui.createLabel("\nAsset author")) + self.cbxAuthors = mhapi.ui.createComboBox(self.authors) + self.filterBox.addWidget(self.cbxAuthors) + + yesno = ["-- any --", "yes", "no"] + + #self.filterBox.addWidget(mhapi.ui.createLabel("\nHas screenshot")) + #self.cbxScreenshot = mhapi.ui.createComboBox(yesno) + #self.filterBox.addWidget(self.cbxScreenshot) + + #self.filterBox.addWidget(mhapi.ui.createLabel("\nHas thumbnail")) + #self.cbxThumb = mhapi.ui.createComboBox(yesno) + #self.filterBox.addWidget(self.cbxThumb) + + # yesno.extend(["with new remote"]) + + lic = ["-- any --","CC0", "CC-BY", "AGPL"] + self.filterBox.addWidget(mhapi.ui.createLabel("\nAsset license")) + self.cbxLicense = mhapi.ui.createComboBox(lic) + self.filterBox.addWidget(self.cbxLicense) + + self.filterBox.addWidget(mhapi.ui.createLabel("\nAlready downloaded")) + self.cbxDownloaded = mhapi.ui.createComboBox(yesno) + self.filterBox.addWidget(self.cbxDownloaded) + + upd = ["-- any --","One week", "One month", "Three months", "One year"] + self.filterBox.addWidget(mhapi.ui.createLabel("\nUpdated/created within")) + self.cbxUpdated = mhapi.ui.createComboBox(upd) + self.filterBox.addWidget(self.cbxUpdated) + + self.filterBox.addWidget(mhapi.ui.createLabel("\nTitle contains")) + self.txtTitle = mhapi.ui.createTextEdit() + self.filterBox.addWidget(self.txtTitle) + + self.filterBox.addWidget(mhapi.ui.createLabel("\nDescription contains")) + self.txtDesc = mhapi.ui.createTextEdit() + self.filterBox.addWidget(self.txtDesc) + + self.filterBox.addWidget(mhapi.ui.createLabel(" ")) + self.btnFilter = mhapi.ui.createButton("Update list") + self.filterBox.addWidget(self.btnFilter) + + @self.btnFilter.mhEvent + def onClicked(event): + self._onBtnFilterClick() + + self.addLeftWidget(self.filterBox) + + def _onTypeChange(self,newValue): + self.log.trace("Enter") + self.log.debug("Asset type changed to",newValue) + + if newValue == "clothes": + self.cbxSubTypes.clear() + self.cbxSubTypes.addItem("-- any --") + for type in self.assetdb.getKnownClothesCategories(): + self.cbxSubTypes.addItem(type) + else: + self.cbxSubTypes.clear() + self.cbxSubTypes.addItem("-- any --") + if newValue == "material": + self.cbxSubTypes.addItem("for core asset") + + + def _onBtnFilterClick(self): + self.log.trace("Enter") + oldlen = len(self.headers) + + author = None + subtype = None + changed = None + license = None + + if self.cbxAuthors.getCurrentItem() != "-- any --": + author = str(self.cbxAuthors.getCurrentItem()) + + assetType = str(self.cbxTypes.getCurrentItem()) + + if assetType == "clothes" or assetType == "material": + subtype = str(self.cbxSubTypes.getCurrentItem()) + if subtype == "-- any --": + subtype = None + + title = str(self.txtTitle.getText()) + if title == "": + title = None + + desc = str(self.txtDesc.getText()) + if desc == "": + desc = None + + if self.cbxLicense.getCurrentItem() != "-- any --": + license = str(self.cbxLicense.getCurrentItem()) + + if self.cbxUpdated.getCurrentItem() != "-- any --": + changed = str(self.cbxUpdated.getCurrentItem()) + + downloaded = str(self.cbxDownloaded.getCurrentItem()) + if downloaded == "-- any --": + downloaded = None + + assets = self.assetdb.getFilteredAssets(assetType, author=author, subtype=subtype, title=title, isDownloaded=downloaded, desc=desc, changed=changed, license=license) + + self.data = [] + + self.headers = ["node id", "author", "license", "title", "description"] + + for asset in assets: + self.data.append( [ str(asset.getId()), asset.getAuthor(), asset.getLicense(), asset.getTitle(), asset.getDescription() ]) + + self.model = AssetTableModel(self.data,self.headers) + self.proxymodel = QSortFilterProxyModel() + self.proxymodel.setSourceModel(self.model) + self.tableView.setModel(self.proxymodel) + + self.tableView.columnCountChanged(oldlen, len(self.headers)) + self.tableView.resizeColumnsToContents() + + self.hasFilter = True + self.currentlySelectedRemoteAsset = None + self.thumbnail.setPixmap(QtGui.QPixmap(os.path.abspath(self.notfound))) + + self.tableView.show() + self.detailsPanel.hide() + self.isShowingDetails = False + self.btnDetails.setText("View details") + + def _setupSelectedBox(self): + self.log.trace("Enter") + self.selectBox = mhapi.ui.createGroupBox("Selected") + + self.thumbnail = self.selectBox.addWidget(gui.TextView()) + self.thumbnail.setPixmap(QtGui.QPixmap(os.path.abspath(self.notfound))) + self.thumbnail.setGeometry(0,0,128,128) + self.thumbnail.setMaximumHeight(128) + self.thumbnail.setMaximumWidth(128) + self.thumbnail.setScaledContents(True) + + self.selectBox.addWidget(mhapi.ui.createLabel(" ")) + + self.btnDetails = mhapi.ui.createButton("View details") + self.selectBox.addWidget(self.btnDetails) + + @self.btnDetails.mhEvent + def onClicked(event): + self._onBtnDetailsClick() + + self.btnDownload = mhapi.ui.createButton("Download") + self.selectBox.addWidget(self.btnDownload) + + @self.btnDownload.mhEvent + def onClicked(event): + self._onBtnDownloadClick() + + self.addRightWidget(self.selectBox) + + def _onBtnDetailsClick(self): + self.log.trace("Enter") + + if self.isShowingDetails: + self.tableView.show() + self.detailsPanel.hide() + self.isShowingDetails = False + self.btnDetails.setText("View details") + return + + if self.currentlySelectedRemoteAsset is None: + self.log.debug("No asset is selected") + return + + title = self.currentlySelectedRemoteAsset.getTitle() + self.log.debug("Request details for asset with title",title) + + self.tableView.hide() + self.detailsPanel.show() + self.btnDetails.setText("Hide details") + self.isShowingDetails = True + + def _onBtnDownloadClick(self): + self.log.trace("Enter") + + if not self.assetdb or not self.assetdb.isSynchronized: + self.log.debug("Database has not been synchronized") + return + + if not self.hasFilter: + self.log.debug("Table is empty") + + if self.currentlySelectedRemoteAsset is None: + self.log.debug("No asset is selected") + return + + title = self.currentlySelectedRemoteAsset.getTitle() + self.log.debug("Request download of asset with title",title) + + self.assetdb.downloadItem(self.syncBox,self.currentlySelectedRemoteAsset,self._downloadItemFinished) + + def _downloadItemFinished(self, code=0, file=None): + if code > 0: + msg = "The requested item failed to download. The server responded with the error code " + str(code) \ + + " when trying to download " + str(file) + ".\n\nThis is an indication that there is something wrong " \ + + "with the asset on the server, and it should probably be reported to the author of the asset, " \ + + "possibly as a comment on the asset page." + self.showMessage(msg) + else: + self.showMessage("Finished downloading") + + def _setupSyncBox(self): + self.log.trace("Enter") + self.syncBox = mhapi.ui.createGroupBox("Synchronize") + + syncinstr = "" + syncinstr = syncinstr + "Before being able to\n" + syncinstr = syncinstr + "list or download any\n" + syncinstr = syncinstr + "assets, you need to\n" + syncinstr = syncinstr + "download the database\n" + syncinstr = syncinstr + "from the server.\n\n" + syncinstr = syncinstr + "Later, you only need\n" + syncinstr = syncinstr + "to do this when you \n" + syncinstr = syncinstr + "want to check for\n" + syncinstr = syncinstr + "new assets.\n\n" + syncinstr = syncinstr + "Optionally you can\n" + syncinstr = syncinstr + "get all screenshots.\n" + syncinstr = syncinstr + "This is hundreds of\n" + syncinstr = syncinstr + "megabytes, so avoid\n" + syncinstr = syncinstr + "unless important.\n" + + self.syncBox.addWidget(mhapi.ui.createLabel(syncinstr)) + + self.fetchScreens = self.syncBox.addWidget(gui.CheckBox('get screenshots')) + + self.btnSync = mhapi.ui.createButton("Synchronize") + self.syncBox.addWidget(self.btnSync) + + @self.btnSync.mhEvent + def onClicked(event): + self._onBtnSyncClick(downloadScreenshots=self.fetchScreens.selected) + + self.addRightWidget(self.syncBox) + + def _onBtnSyncClick(self, downloadScreenshots=False): + self.log.trace("Enter") + self.assetdb.synchronizeRemote(self.syncBox,self._onSyncFinished,self._onSyncProgress, downloadScreenshots=downloadScreenshots) + + def _onSyncFinished(self, code=0, file=None): + self.log.trace("onSyncFinished") + self.showMessage("Asset DB is now synchronized") + + self.authors = ["-- any --"] + self.authors.extend(sorted(self.assetdb.getKnownAuthors(), key=lambda s: s.lower())) + + self.cbxAuthors.clear() + + for author in self.authors: + self.cbxAuthors.addItem(author) + + def _onSyncProgress(self,prog=0.0): + self.log.trace("onSyncProgress") + + def _downloadFinished(self, code=0, file=None): + self.log.trace("Enter") + self.log.debug("Download finished") + + def _setupTable(self): + self.log.trace("Enter") + self.data = [["No filter"]] + self.headers = ["Info"] + + self.model = AssetTableModel(self.data, self.headers) + + layout = QVBoxLayout() + + self.tableView = QTableView() + self.tableView.setModel(self.model) + self.tableView.clicked.connect(self._tableClick) + self.tableView.setSelectionBehavior(QTableView.SelectRows) + if(mhapi.utility.isPython3()): + selmode = QtWidgets.QAbstractItemView.SingleSelection + else: + selmode = QAbstractItemView.SingleSelection + self.tableView.setSelectionMode(selmode) + self.tableView.setSortingEnabled(True) + self.tableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.tableView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + + self.addTopWidget(self.tableView) + + self.hasFilter = False + + def _onBtnDownloadScreenshotClick(self): + self.log.debug("Download screenshot") + remoteAsset = self.currentlySelectedRemoteAsset + tups = remoteAsset.getDownloadTuples(ignoreExisting=True, onlyMeta=True, excludeThumb=True, excludeScreenshot=False) + self.screenshotDt = DownloadTask(self, tups, self._afterScreenshotDownloaded) + + def _afterScreenshotDownloaded(self, code=0, file=None): + self.log.debug("Downloaded") + remoteAsset = self.currentlySelectedRemoteAsset + render = remoteAsset.getScreenshotPath() + + if render is not None and render != "" and os.path.exists(render): + self.detailsRender.setPixmap(QtGui.QPixmap(os.path.abspath(render))) + self.detailsRender.setGeometry(0, 0, 800, 600) + self.btnDownloadScreenshot.hide() + + def _setupDetails(self): + self.log.trace("Enter") + + layout = QVBoxLayout() + + self.detailsName = mhapi.ui.createLabel("

Selected title

") + layout.addWidget(self.detailsName) + + self.detailsDesc = mhapi.ui.createLabel("

Selected description

") + self.detailsDesc.setWordWrap(True) + layout.addWidget(self.detailsDesc) + + self.detailsExtras = mhapi.ui.createLabel("License...: Hej
Category...: Hopp
") + layout.addWidget(self.detailsExtras) + + self.detailsRender = gui.TextView() + self.detailsRender.setPixmap(QPixmap(os.path.abspath(self.notfound))) + layout.addWidget(self.detailsRender) + + self.btnDownloadScreenshot = mhapi.ui.createButton("Download screenshot") + + @self.btnDownloadScreenshot.mhEvent + def onClicked(event): + self._onBtnDownloadScreenshotClick() + + blayout = QHBoxLayout() + bwidget = QWidget() + bwidget.setLayout(blayout) + blayout.addWidget(self.btnDownloadScreenshot) + blayout.addStretch(1) + + layout.addWidget(bwidget) + layout.addStretch(1) + + self.detailsPanel = QWidget() + self.detailsPanel.setLayout(layout) + + self.addTopWidget(self.detailsPanel) + self.detailsPanel.hide() + + def _tableClick(self): + + self.log.trace("Table click") + + if not self.hasFilter: + return + + currentRow = None + + indexes = self.tableView.selectionModel().selectedRows() + for index in sorted(indexes): + currentRow = index + + if currentRow is None: + self.log.debug("No row is selected") + return + + self.log.debug("Currently selected row", currentRow) + + currentRow = self.proxymodel.mapToSource(currentRow) + + self.log.debug("Currently selected mapped row", currentRow) + + currentRow = currentRow.row() + + self.log.debug("Currently selected mapped row index", currentRow) + + self.log.spam("Currently selected row data", self.data[currentRow]) + + assetId = int(self.data[currentRow][0]) + assetType = str(self.cbxTypes.getCurrentItem()) + + self.log.debug("Currently selected asset id", assetId) + + remoteAsset = self.assetdb.remoteAssets[assetType][assetId] + + thumbPath = remoteAsset.getThumbPath() + + if thumbPath is not None: + self.thumbnail.setPixmap(QtGui.QPixmap(os.path.abspath(thumbPath))) + else: + self.log.debug("Asset has no thumbnail") + + self.thumbnail.setGeometry(0, 0, 128, 128) + + self.currentlySelectedRemoteAsset = remoteAsset + + self.detailsName.setText("

" + remoteAsset.getTitle() + "

") + self.detailsDesc.setText("

" + remoteAsset.getDescription() + "

") + + extras = "

" + extras = extras + "Author........: " + remoteAsset.getAuthor() + "
" + extras = extras + "License.......: " + remoteAsset.getLicense() + "
" + extras = extras + "Last changed..: " + remoteAsset.getChanged() + "
" + + extras = extras + "
" + self.detailsExtras.setText(extras) + + render = remoteAsset.getScreenshotPath() + + if render is not None and render != "" and os.path.exists(render): + self.detailsRender.setPixmap(QtGui.QPixmap(os.path.abspath(render))) + self.detailsRender.setGeometry(0, 0, 800, 600) + self.btnDownloadScreenshot.hide() + else: + self.detailsRender.setPixmap(QtGui.QPixmap(mhapi.locations.getSystemDataPath("notfound.thumb"))) + self.btnDownloadScreenshot.show() + + + def showMessage(self,message,title="Information"): + self.msg = QMessageBox() + self.msg.setIcon(QMessageBox.Information) + self.msg.setText(message) + self.msg.setWindowTitle(title) + self.msg.setStandardButtons(QMessageBox.Ok) + self.msg.show() + diff --git a/makehuman/plugins/8_asset_downloader/downloadtask.py b/makehuman/plugins/8_asset_downloader/downloadtask.py new file mode 100644 index 000000000..185832a43 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/downloadtask.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** .. + +**Product Home Page:** TBD + +**Code Home Page:** TBD + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +""" + +import gui3d +import mh +import socket +import json +import os +import time +import sys +import io +import urllib + +from progress import Progress + +mhapi = gui3d.app.mhapi + +qtSignal = None +qtSlot = None + +if mhapi.utility.isPython3(): + from PyQt5 import QtGui + from PyQt5 import QtCore + from PyQt5.QtGui import * + from PyQt5 import QtWidgets + from PyQt5.QtWidgets import * + from PyQt5.QtCore import * + qtSignal = QtCore.pyqtSignal + qtSlot = QtCore.pyqtSlot +else: + if mhapi.utility.isPySideAvailable(): + from PySide import QtGui + from PySide import QtCore + from PySide.QtGui import * + from PySide.QtCore import * + qtSignal = QtCore.Signal + qtSlot = QtCore.Slot + else: + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import * + from PyQt4.QtCore import * + qtSignal = QtCore.pyqtSignal + qtSlot = QtCore.pyqtSlot + + +class DownloadThread(QThread): + + signalProgress = qtSignal(float) + signalFinished = qtSignal(str) + + def __init__(self, downloadTuples, parent = None, overrideProgressSteps=None): + QThread.__init__(self, parent) + self.log = mhapi.utility.getLogChannel("assetdownload") + self.exiting = False + self.downloadTuples = downloadTuples + self.log.debug("Downloadtuples length:",len(downloadTuples)) + self.request = mhapi.utility.getCompatibleUrlFetcher() + self.overrideProgressSteps = overrideProgressSteps + + def run(self): + self.log.trace("Enter") + self.onProgress(0.0) + + total = len(self.downloadTuples) + current = 0 + + lastReport = time.time() + + downloadStatus = "OK" + + for dt in self.downloadTuples: + remote = dt[0] + local = dt[1] + dn = os.path.dirname(local) + if not os.path.exists(dn): + os.makedirs(dn) + current = current + 1 + self.log.trace("About to download", remote) + self.log.trace("Destination is", local) + + remote = remote.replace(" ", "%20") + + try: + requrl = self.request.urlopen(remote) + cl = requrl.info().get('Content-Length').strip() + self.log.debug("Content length", cl) + + megabytes = 0 + + if cl: + if mhapi.utility.isPy3 and str(cl).isnumeric(): + megabytes = float(cl) / 1000000.0 + if not mhapi.utility.isPy3 and unicode(cl).isnumeric(): + megabytes = float(cl) / 1000000.0 + + self.log.debug("Content megabytes", megabytes) + + if megabytes < 1.0: + self.log.debug("File to be downloaded in one chunk, size is less than one meg") + data = requrl.read() + with open(local,"wb") as f: + f.write(data) + self.log.debug("Successfully downloaded",remote) + else: + # Very large file + self.log.info("File to be downloaded in chunks, size is", megabytes) + buf = io.BytesIO() + size = 0 + megabytes = int(int(cl) / 1000000) + 1 + while True: + buf1 = requrl.read(100 * 1000) # 100kb size blocks + if not buf1: + break + buf.write(buf1) + size += len(buf1) + self.log.spam("Downloaded buffer size",size) + sizemegs = int(size / 1000000) + + now = time.time() + now = now - 0.5 + if now > lastReport: + lastReport = now + fileProgress = float(sizemegs) / float(megabytes) + fileProgress = float(current - 1) + fileProgress + if self.overrideProgressSteps is None: + self.onProgress(float(fileProgress) / float(total)) + else: + self.onProgress(float(fileProgress) / float(self.overrideProgressSteps)) + with open(local,"wb") as f: + f.write(buf.getvalue()) + self.log.debug("Successfully downloaded",remote) + + except urllib.error.HTTPError as e: + self.log.error("Caught http error", e) + downloadStatus = str(e.code) + ";" + remote + break + except: + self.log.error("Exception in download",sys.exc_info()) + self.log.warn("Could not download",remote) + + now = time.time() + now = now - 0.5 + if now > lastReport: + lastReport = now + if self.overrideProgressSteps is None: + self.onProgress(float(current) / float(total)) + else: + self.onProgress(float(current) / float(self.overrideProgressSteps)) + + self.onFinished(downloadStatus) + self.exiting = True + + def onProgress(self, prog = 0.0): + self.log.trace("onProgress",prog) + self.signalProgress.emit(prog) + + def onFinished(self, status = "OK"): + self.log.trace("Enter") + self.signalFinished.emit(status) + + def __del__(self): + self.log.trace("Enter") + self.exiting = True + self.log = None + self.downloadTuples = None + self.request = None + + +class DownloadTask(): + + def __init__(self, parentWidget, downloadTuples, onFinished=None, onProgress=None, overrideProgressSteps=None): + self.log = mhapi.utility.getLogChannel("assetdownload") + + self.parentWidget = parentWidget + self.onFinished = onFinished + self.onProgress = onProgress + self.overrideProgressSteps = overrideProgressSteps + + self.downloadThread = DownloadThread(downloadTuples, overrideProgressSteps = self.overrideProgressSteps) + + self.downloadThread.signalProgress.connect(self._onProgress) + self.downloadThread.signalFinished.connect(self._onFinished) + + self.progress = Progress() + + self.log.debug("About to start downloading") + self.log.spam("downloadTuples",downloadTuples) + + self.downloadThread.start() + + def _onProgress(self, prog=0.0): + self.log.trace("_onProgress",prog) + + self.progress(prog,desc="Downloading files...") + + if self.onProgress is not None: + self.log.trace("onProgress callback is defined") + self.onProgress(prog) + else: + self.log.trace("onProgress callback is not defined") + + def _onFinished(self, status = "OK"): + self.log.trace("Enter") + self.log.debug("Status", status) + + if self.overrideProgressSteps is None: + self.progress(1.0) + + self.downloadThread.signalProgress.disconnect(self._onProgress) + self.downloadThread.signalFinished.disconnect(self._onFinished) + + if self.onFinished is not None: + self.log.trace("onFinished callback is defined") + + code = 0; file = None + + if status != "OK": + (code, file) = status.split(";",2) + code = int(code) + self.onFinished(code, file) + + else: + self.log.trace("onFinished callback is not defined") + + self.downloadThread = None diff --git a/makehuman/plugins/8_asset_downloader/meshAssetSubdirs.py b/makehuman/plugins/8_asset_downloader/meshAssetSubdirs.py new file mode 100644 index 000000000..8db251ad0 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/meshAssetSubdirs.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 + +# These are the subdirs that can contain third-part materials for +# core assets. + +ALL_CORE_ASSET_DIRS_WITH_MATERIALS_AS_TEXT=''' +clothes/fedora01 +clothes/female_casualsuit01 +clothes/female_casualsuit02 +clothes/female_elegantsuit01 +clothes/female_sportsuit01 +clothes/male_casualsuit01 +clothes/male_casualsuit02 +clothes/male_casualsuit03 +clothes/male_casualsuit04 +clothes/male_casualsuit05 +clothes/male_casualsuit06 +clothes/male_elegantsuit01 +clothes/male_worksuit01 +clothes/shoes01 +clothes/shoes02 +clothes/shoes03 +clothes/shoes04 +clothes/shoes05 +clothes/shoes06 +eyebrows/eyebrow001 +eyebrows/eyebrow002 +eyebrows/eyebrow003 +eyebrows/eyebrow004 +eyebrows/eyebrow005 +eyebrows/eyebrow006 +eyebrows/eyebrow007 +eyebrows/eyebrow008 +eyebrows/eyebrow009 +eyebrows/eyebrow010 +eyebrows/eyebrow011 +eyebrows/eyebrow012 +eyelashes/eyelashes01 +eyelashes/eyelashes02 +eyelashes/eyelashes03 +eyelashes/eyelashes04 +eyes/high-poly +eyes/low-poly +hair/afro01 +hair/bob01 +hair/bob02 +hair/braid01 +hair/long01 +hair/ponytail01 +hair/short01 +hair/short02 +hair/short03 +hair/short04 +teeth/teeth_base +teeth/teeth_shape01 +teeth/teeth_shape02 +teeth/teeth_shape03 +teeth/teeth_shape04 +teeth/teeth_shape05 +tongue/tongue01 +''' + diff --git a/makehuman/plugins/8_asset_downloader/remoteasset.py b/makehuman/plugins/8_asset_downloader/remoteasset.py new file mode 100644 index 000000000..c45ae4f19 --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/remoteasset.py @@ -0,0 +1,321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** http://www.makehumancommunity.org + +**Code Home Page:** https://github.com/makehumancommunity/community-plugins + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import json +import os +import re +import platform +import calendar, datetime + +from progress import Progress + +from core import G + +from .meshAssetSubdirs import ALL_CORE_ASSET_DIRS_WITH_MATERIALS_AS_TEXT + +mhapi = gui3d.app.mhapi + +fileForType = {} +fileForType["material"] = "mhmat" +fileForType["model"] = "mhm" +fileForType["clothes"] = "mhclo" +fileForType["hair"] = "mhclo" +fileForType["teeth"] = "mhclo" +fileForType["eyebrows"] = "mhclo" +fileForType["eyelashes"] = "mhclo" +fileForType["tongue"] = "mhclo" +fileForType["eyes"] = "mhclo" +fileForType["proxy"] = "file" +fileForType["skin"] = "mhmat" +fileForType["pose"] = "bvh" +fileForType["expression"] = "mhpose" +fileForType["rig"] = "mhskel" +fileForType["target"] = "file" + + +class RemoteAsset(): + + def __init__(self, parent, json, assetdb=None): + + self.assetdb = assetdb + self.cachedDestination = None + self.parent = parent + self.rawJson = json + self.log = mhapi.utility.getLogChannel("assetdownload") + + self._parseGeneric() + + if self.type == "clothes": + self._parseClothes() + + if self.type == "material": + self._parseMaterials() + + self.root = os.path.join(self.parent.root,str(self.nid)) + if not os.path.exists(self.root): + os.makedirs(self.root) + + self._parseFiles() + + def _getJsonKey(self,name,default): + + self.log.spam("Enter") + + out = default + if name in self.rawJson: + out = self.rawJson[name] + return out + + def _parseGeneric(self): + + self.log.trace("Enter") + + self.type = self._getJsonKey("type", "unknown") + self.license = self._getJsonKey("license", "unknown") + self.title = self._getJsonKey("title","-- unknown title --") + self.description = self._getJsonKey("description", "--") + self.username = self._getJsonKey("username","unknown author") + self.uid = self._getJsonKey("uid",-1) + self.nid = self._getJsonKey("nid",-1) + self.changed = self._getJsonKey("changed",None) + self.created = self._getJsonKey("created",None) + + def _parseClothes(self): + + self.category = self._getJsonKey("category", "").lower() + + if self.category == "eyebrows": + self.type = "eyebrows" + + if self.category == "eyelashes": + self.type = "eyelashes" + + if self.category == "teeth": + self.type = "teeth" + + if self.category == "hair": + self.type = "hair" + + def _parseMaterials(self): + + self.belongs_to_metadata = self._getJsonKey("belongs_to", {}) + + self.log.spam("belonging", self.belongs_to_metadata) + + if not "belonging_is_assigned" in self.belongs_to_metadata or not self.belongs_to_metadata["belonging_is_assigned"]: + self.belongs_to_metadata["belonging_is_assigned"] = False + return + + if "belongs_to_core_asset" in self.belongs_to_metadata: + self.log.trace("Belongs to core asset") + + caTarget = self.belongs_to_metadata["belongs_to_core_asset"].strip() + if not caTarget or caTarget == "" or not caTarget in ALL_CORE_ASSET_DIRS_WITH_MATERIALS_AS_TEXT: + self.log.debug("Assigned, but not in permitted list: ", caTarget) + return + else: + self.log.debug("Permitted for core asset: ", caTarget) + udp = mhapi.locations.getUserDataPath() + parts = caTarget.split("/") + self.cachedDestination = os.path.abspath( os.path.join(udp, parts[0], parts[1]) ) + self.log.debug("Override installation path",self.cachedDestination) + + if "belongs_to_id" in self.belongs_to_metadata: + if self.belongs_to_metadata["belongs_to_id"] in self.assetdb.assetsById: + targetAsset = self.assetdb.assetsById[self.belongs_to_metadata["belongs_to_id"]] + self.log.debug("Target asset", targetAsset.getTitle()) + self.log.debug("Target asset ID", targetAsset.getId()) + self.log.debug("Original installation path", self.getInstallPath()) + ip = targetAsset.getInstallPath() + self.log.debug("Overriding target installation path", ip) + self.cachedDestination = ip + + def _parseFiles(self): + + self.log.trace("Enter") + + self.remoteFiles = {} + self.localFiles = {} + + if not "files" in self.rawJson: + return + + self.log.spam("Files key in json",self.rawJson["files"]) + + for ftype in self.rawJson["files"].keys(): + + name = ftype + + if name == "illustration": + name = "screenshot" + if name == "render": + name = "screenshot" + + self.remoteFiles[name] = self.rawJson["files"][ftype] + + self.log.trace("Remote file",self.remoteFiles[name]) + + fn = self.remoteFiles[name].rsplit('/', 1)[-1] + + self.log.spam("fn 1",fn) + + extension = os.path.splitext(fn)[1] + extension = extension.lower() + + if name == "screenshot": + convertedScreenshot = os.path.join(self.root, "screenshot.jpg") + if os.path.exists(convertedScreenshot): + self.localFiles[name] = convertedScreenshot + else: + fn = "screenshot" + extension + self.localFiles[name] = os.path.join(self.root, fn) + + if name == "thumb": + fn = "thumb.png" + self.localFiles[name] = os.path.join(self.root, fn) + + self.log.spam("fn 2", fn) + + if not name == "screenshot" and not name == "thumb": + ip = self.getInstallPath() + self.log.trace("Install path",ip) + self.localFiles[name] = os.path.join(ip,fn) + + self.log.spam("remoteFiles",self.remoteFiles) + self.log.spam("localFiles", self.localFiles) + + def getCategory(self): + self.log.trace("Enter") + return self.category + + def getType(self): + self.log.trace("Enter") + return self.type + + def getDescription(self): + return self.description + + def getId(self): + self.log.trace("Enter") + return self.nid + + def getTitle(self): + self.log.trace("Enter") + return self.title + + def getUsername(self): + self.log.trace("Enter") + return self.username + + def getAuthor(self): + self.log.trace("Enter") + return self.username + + def getChanged(self): + self.log.trace("Enter") + return self.changed + + def getCreated(self): + self.log.trace("Enter") + return self.created + + def getLicense(self): + self.log.trace("Enter") + return self.license + + def getPertinentFileName(self): + self.log.trace("Enter") + key = fileForType[self.type] + if key in self.remoteFiles: + fn = self.remoteFiles[key].rsplit('/', 1)[-1] + self.log.trace("Pertinent file",fn) + return fn + else: + self.log.warn("Could not find pertinent file for asset",self.title) + self.log.warn("File type is",self.type) + self.log.warn("Files contain",self.remoteFiles) + + return None + + def getScreenshotPath(self): + self.log.trace("Enter") + if "screenshot" in self.localFiles: + return self.localFiles["screenshot"] + else: + return None + + def getThumbPath(self): + self.log.trace("Enter") + if "thumb" in self.localFiles: + return self.localFiles["thumb"] + else: + return None + + def getInstallPath(self): + self.log.trace("Enter") + if self.cachedDestination is None: + self.cachedDestination = mhapi.assets.getAssetLocation(self.title, self.type) + return self.cachedDestination + + def getDownloadTuples(self, ignoreExisting = True, onlyMeta=False, excludeThumb=False, excludeScreenshot=False): + self.log.trace("Enter") + downloads = [] + for key in self.remoteFiles.keys(): + l = self.localFiles[key] + r = self.remoteFiles[key] + + self.log.trace("l",l) + self.log.trace("r",r) + + self.log.trace("key",key) + + if not ignoreExisting or not os.path.exists(l): + + if key == "thumb" or key == "render" or key == "screenshot": + self.log.trace("ismeta") + if key == "thumb": + if not excludeThumb: + self.log.trace("Adding file for download", r) + downloads.append((r, l)) + else: + self.log.trace("Exclude due to excludeThumb",r) + else: + if not excludeScreenshot: + self.log.trace("Adding file for download", r) + downloads.append((r, l)) + else: + self.log.trace("Exclude due to excludeScreenshot",r) + else: + if not onlyMeta: + self.log.trace("Adding file for download",r) + downloads.append((r, l)) + else: + self.log.trace("Exclude due to onlyMeta",r) + else: + self.log.trace("Ignoring file",r) + + return downloads + diff --git a/makehuman/plugins/8_asset_downloader/tablemodel.py b/makehuman/plugins/8_asset_downloader/tablemodel.py new file mode 100644 index 000000000..ae937623d --- /dev/null +++ b/makehuman/plugins/8_asset_downloader/tablemodel.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community assets + +**Product Home Page:** http://www.makehumancommunity.org + +**Code Home Page:** https://github.com/makehumancommunity/community-plugins + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin manages community assets + +""" + +import gui3d +import mh +import gui +import json +import os +import re +import platform +import calendar +import datetime + +from progress import Progress + +from core import G + +mhapi = gui3d.app.mhapi + +if mhapi.utility.isPython3(): + from PyQt5 import QtGui + from PyQt5 import QtCore + from PyQt5.QtGui import * + from PyQt5 import QtWidgets + from PyQt5.QtWidgets import * + from PyQt5.QtCore import * +else: + if mhapi.utility.isPySideAvailable(): + from PySide import QtGui + from PySide import QtCore + from PySide.QtGui import * + from PySide.QtCore import * + else: + from PyQt4 import QtGui + from PyQt4 import QtCore + from PyQt4.QtGui import * + from PyQt4.QtCore import * + +class AssetTableModel(QAbstractTableModel): + + def __init__(self, data, headers, parent=None): + QAbstractTableModel.__init__(self,parent) + self.log = mhapi.utility.getLogChannel("assetdownload") + + self.__data=data # Initial Data + self.__headers=headers + + def rowCount( self, parent ): + self.log.trace("rowCount") + return len(self.__data) + + def columnCount( self , parent ): + self.log.trace("columnCount") + return len(self.__headers) + + def data ( self , index , role ): + if role == Qt.DisplayRole: + row = index.row() + column = index.column() + value = self.__data[row][column] + + if mhapi.utility.isPython3(): + return str(value) + else: + return QString(value) + + def headerData(self, section, orientation = Qt.Horizontal, role=Qt.DisplayRole): + if role == Qt.DisplayRole and self.__headers is not None: + if orientation == Qt.Horizontal: + if mhapi.utility.isPython3(): + return self.__headers[section] + else: + return QString(self.__headers[section]) + else: + if mhapi.utility.isPython3(): + return str(section + 1) + else: + return QString(str(section + 1)) + diff --git a/makehuman/plugins/8_server_socket/__init__.py b/makehuman/plugins/8_server_socket/__init__.py new file mode 100644 index 000000000..7ddb3b1dd --- /dev/null +++ b/makehuman/plugins/8_server_socket/__init__.py @@ -0,0 +1,227 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman server socket plugin + +**Product Home Page:** TBD + +**Code Home Page:** TBD + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2018 + +**Licensing:** MIT + +Abstract +-------- + +This plugin opens a TCP socket and accepts some basic commands. It +does not make much sense without a corresponding client. + +""" + +import gui3d +import mh +import gui +import socket +import json +import sys +import getpath +import os + +mhapi = gui3d.app.mhapi +isPy3 = mhapi.utility.isPy3 + +if isPy3: + from .dirops import SocketDirOps + from .meshops import SocketMeshOps + from .modops import SocketModifierOps + from .workerthread import WorkerThread + +class SocketTaskView(gui3d.TaskView): + + def __init__(self, category, socketConfig=None): + + self.human = gui3d.app.selectedHuman + gui3d.TaskView.__init__(self, category, 'Socket') + + self.socketConfig = {'acceptConnections': False, + 'advanced': False, + 'host': '127.0.0.1', + 'port': 12345 } + + if socketConfig and isinstance(socketConfig, dict): + self.socketConfig['acceptConnections'] = socketConfig.get('acceptConnections', False) + self.socketConfig['advanced'] = socketConfig.get('advanced', False) + self.socketConfig['host'] = socketConfig.get('host', '127.0.0.1') + self.socketConfig['port'] = socketConfig.get('port', 12345) + + self.workerthread = None + + self.log = mhapi.utility.getLogChannel("socket") + + box = self.addLeftWidget(gui.GroupBox('Server')) + + self.accToggleButton = box.addWidget(gui.CheckBox('Accept connections')) + box.addWidget(mhapi.ui.createLabel('')) + self.advToggleButton = box.addWidget(gui.CheckBox('Advanced Setings')) + self.hostLabel = box.addWidget(mhapi.ui.createLabel('\nHost [Default=127.0.0.1] :')) + self.hostEdit = box.addWidget(gui.TextEdit(str(self.socketConfig.get('host')))) + self.portLabel = box.addWidget(mhapi.ui.createLabel('\nPort [Default=12345] :')) + self.portEdit = box.addWidget(gui.TextEdit(str(self.socketConfig.get('port')))) + self.spacer = box.addWidget(mhapi.ui.createLabel('')) + self.changeAddrButton = box.addWidget(gui.Button('Change Host + Port')) + + self.hostEdit.textChanged.connect(self.onHostChanged) + self.portEdit.textChanged.connect(self.onPortChanged) + + @self.accToggleButton.mhEvent + def onClicked(event): + if isPy3: + if self.accToggleButton.selected: + self.socketConfig['acceptConnections'] = True + self.openSocket() + else: + self.socketConfig['acceptConnections'] = False + self.closeSocket() + + @self.advToggleButton.mhEvent + def onClicked(event): + self.enableAdvanced(self.advToggleButton.selected) + self.socketConfig['advanced'] = self.advToggleButton.selected + + @self.changeAddrButton.mhEvent + def onClicked(event): + if isPy3: + gui3d.app.prompt('Attention', 'The host and port must be changed in Blender, too', 'OK', + helpId='socketInfo') + self.accToggleButton.setChecked(True) + self.closeSocket() + self.openSocket() + + + self.scriptText = self.addTopWidget(gui.DocumentEdit()) + if isPy3: + self.scriptText.setText('') + else: + self.scriptText.setText('This version of the socket plugin requires the py3 version of MH from github.') + self.scriptText.setLineWrapMode(gui.DocumentEdit.NoWrap) + + if isPy3: + self.dirops = SocketDirOps(self) + self.meshops = SocketMeshOps(self) + self.modops = SocketModifierOps(self) + if self.socketConfig.get('acceptConnections'): + self.accToggleButton.setChecked(True) + self.openSocket() + self.enableAdvanced(self.socketConfig.get('advanced', False)) + self.advToggleButton.setChecked(self.socketConfig.get('advanced', False)) + + + def onHostChanged(self): + self.socketConfig['host'] = self.hostEdit.text + + def onPortChanged(self): + text = str(self.portEdit.text) + if text.isdigit(): + self.socketConfig['port'] = int(text) + else: + self.portEdit.setText(str(self.socketConfig.get('port'))) + + def enableAdvanced(self, state = False): + if state: + self.hostLabel.show() + self.hostEdit.show() + self.portLabel.show() + self.portEdit.show() + self.spacer.show() + self.changeAddrButton.show() + else: + self.hostLabel.hide() + self.hostEdit.hide() + self.portLabel.hide() + self.portEdit.hide() + self.spacer.hide() + self.changeAddrButton.hide() + + def threadMessage(self,message): + self.addMessage(str(message)) + + def evaluateCall(self): + ops = None + data = self.workerthread.jsonCall + conn = self.workerthread.currentConnection + + if self.meshops.hasOp(data.function): + ops = self.meshops + + if self.dirops.hasOp(data.function): + ops = self.dirops + + if self.modops.hasOp(data.function): + ops = self.modops + + if ops: + jsonCall = ops.evaluateOp(conn,data) + else: + jsonCall = data + jsonCall.error = "Unknown command" + + if not jsonCall.responseIsBinary: + self.addMessage("About to serialize JSON. This might take some time.") + response = jsonCall.serialize() + #print("About to send:\n\n" + response) + response = bytes(response, encoding='utf-8') + else: + response = jsonCall.data + #print("About to send binary response with length " + str(len(response))) + + conn.send(response) + conn.close() + + def addMessage(self,message,newLine = True): + self.log.debug("addMessage: ", message) + if newLine: + message = message + "\n" + self.scriptText.addText(message) + + def openSocket(self): + self.addMessage("Starting server thread.") + self.workerthread = WorkerThread(socketConfig=self.socketConfig) + self.workerthread.signalEvaluateCall.connect(self.evaluateCall) + self.workerthread.signalAddMessage.connect(self.threadMessage) + self.workerthread.start() + + def closeSocket(self): + #self.addMessage("Closing socket.") + if self.workerthread: + self.workerthread.stopListening() + self.workerthread = None + + +category = None +taskview = None +cfgFile = os.path.join(getpath.getPath(),'socket.cfg') + + +def load(app): + socketConfig = {} + if os.path.isfile(cfgFile): + try: + with open(cfgFile, 'r', encoding='utf-8') as f: + socketConfig = json.loads(f.read()) + except json.JSONDecodeError: + socketConfig = None + category = app.getCategory('Community') + taskview = category.addTask(SocketTaskView(category, socketConfig=socketConfig)) + + +def unload(app): + category = app.getCategory('Community') + taskview = category.getTaskByName('Socket') + if taskview: + taskview.closeSocket() + with open(cfgFile, 'w', encoding='utf-8') as f: + f.writelines(json.dumps(taskview.socketConfig, indent=4)) diff --git a/makehuman/plugins/8_server_socket/abstractop.py b/makehuman/plugins/8_server_socket/abstractop.py new file mode 100644 index 000000000..19eea0960 --- /dev/null +++ b/makehuman/plugins/8_server_socket/abstractop.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +from sys import exc_info +import traceback +import sys + +from core import G + +class AbstractOp(): + + def __init__(self, sockettaskview): + self.parent = sockettaskview + self.functions = dict() + self.human = sockettaskview.human + self.api = G.app.mhapi + + def hasOp(self,function): + return function in self.functions.keys() + + def evaluateOp(self,conn,jsoncall): + + try: + function = jsoncall.getFunction() + + if function in self.functions.keys(): + self.functions[function](conn,jsoncall) + else: + self.parent.addMessage("Did not understand '" + function + "'") + jsoncall.setError('"' + function + '" is not valid command') + except: + print("Exception in JSON:") + print('-'*60) + traceback.print_exc(file=sys.stdout) + print('-'*60) + ex = exc_info() + jsoncall.setError("runtime exception: " + str(ex[1])) + print(ex) + + return jsoncall + + diff --git a/makehuman/plugins/8_server_socket/dirops.py b/makehuman/plugins/8_server_socket/dirops.py new file mode 100644 index 000000000..de6fe1c8d --- /dev/null +++ b/makehuman/plugins/8_server_socket/dirops.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import mh +import os + +from .abstractop import AbstractOp + +class SocketDirOps(AbstractOp): + + def __init__(self, sockettaskview): + super().__init__(sockettaskview) + self.functions["getUserDir"] = self.getUserDir + self.functions["getSysDir"] = self.getSysDir + + def getUserDir(self,conn,jsonCall): + jsonCall.data = os.path.abspath(mh.getPath()) + + def getSysDir(self,conn,jsonCall): + jsonCall.data = os.path.abspath(mh.getSysPath()) + + diff --git a/makehuman/plugins/8_server_socket/meshops.py b/makehuman/plugins/8_server_socket/meshops.py new file mode 100644 index 000000000..66af99069 --- /dev/null +++ b/makehuman/plugins/8_server_socket/meshops.py @@ -0,0 +1,519 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +import os +import pprint +import math +import numpy as np +import time + +from transformations import quaternion_from_matrix +from .abstractop import AbstractOp +from core import G +from material import getSkinBlender + +pp = pprint.PrettyPrinter(indent=4) + +class SocketMeshOps(AbstractOp): + + def __init__(self, sockettaskview): + super().__init__(sockettaskview) + + # Sync operations + self.functions["getCoord"] = self.getCoord + self.functions["getPose"] = self.getPose + + # Import body operations + self.functions["getBodyFacesBinary"] = self.getBodyFacesBinary + self.functions["getBodyMaterialInfo"] = self.getBodyMaterialInfo + self.functions["getBodyMeshInfo"] = self.getBodyMeshInfo + self.functions["getBodyVerticesBinary"] = self.getBodyVerticesBinary + self.functions["getBodyTextureCoordsBinary"] = self.getBodyTextureCoordsBinary + self.functions["getBodyFaceUVMappingsBinary"] = self.getBodyFaceUVMappingsBinary + self.functions["getBodyWeightInfo"] = self.getBodyWeightInfo + self.functions["getBodyWeightsVertList"] = self.getBodyWeightsVertList + self.functions["getBodyWeights"] = self.getBodyWeights + + # Import proxy operations + self.functions["getProxiesInfo"] = self.getProxiesInfo + self.functions["getProxyFacesBinary"] = self.getProxyFacesBinary + self.functions["getProxyMaterialInfo"] = self.getProxyMaterialInfo + self.functions["getProxyVerticesBinary"] = self.getProxyVerticesBinary + self.functions["getProxyTextureCoordsBinary"] = self.getProxyTextureCoordsBinary + self.functions["getProxyFaceUVMappingsBinary"] = self.getProxyFaceUVMappingsBinary + self.functions["getProxyWeightInfo"] = self.getProxyWeightInfo + self.functions["getProxyWeightsVertList"] = self.getProxyWeightsVertList + self.functions["getProxyWeights"] = self.getProxyWeights + + + # Import skeleton operations + self.functions["getSkeleton"] = self.getSkeleton + + def getCoord(self,conn,jsonCall): + jsonCall.data = self.human.mesh.coord + + def getBodyVerticesBinary(self,conn,jsonCall): + jsonCall.responseIsBinary = True + coord = self._getBodyMesh().coord + jsonCall.data = coord.tobytes() + + def _getProxyByUUID(self,strUuid): + for p in self.api.mesh.getAllProxies(includeBodyProxy=True): + if p.uuid == strUuid: + return p + return None + + def getProxiesInfo(self,conn,jsonCall): + objects = [] + + allProxies =self.api.mesh.getAllProxies(includeBodyProxy=False) + + if not self.human.proxy is None and not self.human.proxy.name is None: + # print ("Proxy appended: " + self.human.proxy.name) + allProxies.append(self.human.proxy) + + for p in allProxies: + info = {} + + face_mask = [] + mesh = p.object.getSeedMesh() + + # TODO: Figure out how to find hidden faces on clothes + if p.type == "Proxymeshes": + face_mask = self._boolsToRunLenghtIdx(self.human._Object__proxyMesh.face_mask) + + info["faceMask"] = face_mask + info["type"] = p.type + info["uuid"] = p.uuid + info["name"] = p.name + coord = mesh.coord + shape = coord.shape + info["numVertices"] = shape[0] + info["verticesTypeCode"] = self.api.internals.numpyTypecodeToPythonTypeCode(coord.dtype.str) + info["verticesBytesWhenPacked"] = coord.itemsize * coord.size + faces = mesh.fvert + shape = faces.shape + info["numFaces"] = shape[0] + info["facesTypeCode"] = self.api.internals.numpyTypecodeToPythonTypeCode(faces.dtype.str) + info["facesBytesWhenPacked"] = faces.itemsize * faces.size + objects.append(info) + coord = mesh.texco + shape = coord.shape + info["numTextureCoords"] = shape[0] + info["textureCoordsTypeCode"] = self.api.internals.numpyTypecodeToPythonTypeCode(coord.dtype.str) + info["textureCoordsBytesWhenPacked"] = coord.itemsize * coord.size + fuvs = mesh.fuvs + shape = fuvs.shape + info["numFaceUVMappings"] = shape[0] + info["faceUVMappingsTypeCode"] = self.api.internals.numpyTypecodeToPythonTypeCode(fuvs.dtype.str) + info["faceUVMappingsBytesWhenPacked"] = fuvs.itemsize * fuvs.size + + jsonCall.data = objects + + def getBodyFacesBinary(self,conn,jsonCall): + jsonCall.responseIsBinary = True + faces = self._getBodyMesh().fvert + jsonCall.data = faces.tobytes() + + def getBodyMaterialInfo(self,conn,jsonCall): + if self.human.material.name == 'XrayMaterial' and self.human._backUpMaterial: + material = self.human._backUpMaterial + else: + material = self.human.material + jsonCall.data = self.api.assets.materialToHash(material) + + def getBodyTextureCoordsBinary(self,conn,jsonCall): + jsonCall.responseIsBinary = True + texco = self._getBodyMesh().texco + jsonCall.data = texco.tobytes() + + def getBodyFaceUVMappingsBinary(self,conn,jsonCall): + jsonCall.responseIsBinary = True + faces = self._getBodyMesh().fuvs + jsonCall.data = faces.tobytes() + + def _getBodyMesh(self): + return self.human._Object__seedMesh + + def _boolsToRunLenghtIdx(self, boolArray): + out = [] + i = 0 + needNewRun = True + + while i < len(boolArray): + if boolArray[i]: + if needNewRun: + out.append([i,i]) + needNewRun = False + out[ len(out) - 1 ][1] = i + else: + needNewRun = True + i = i + 1 + + return out + + + def getBodyMeshInfo(self,conn,jsonCall): + jsonCall.data = {} + + filename = "untitled" + + if not G.app.currentFile.title is None: + filename = G.app.currentFile.title + + name = G.app.selectedHuman.getName() + if not name: + name = filename + + jsonCall.data["name"] = name + jsonCall.data["filename"] = filename + + mesh = self._getBodyMesh() + + face_mask = [] + if hasattr(mesh, "face_mask"): + face_mask = self._boolsToRunLenghtIdx(mesh.face_mask) + + jsonCall.data["faceMask"] = face_mask + + coord = mesh.coord + shape = coord.shape + jsonCall.data["numVertices"] = shape[0] + jsonCall.data["verticesTypeCode"] = coord.dtype.str + jsonCall.data["verticesShape"] = coord.shape + jsonCall.data["verticesBytesWhenPacked"] = coord.itemsize * coord.size + + faces = mesh.fvert + shape = faces.shape + + jsonCall.data["numFaces"] = shape[0] + jsonCall.data["facesTypeCode"] = faces.dtype.str + jsonCall.data["facesShape"] = faces.shape + jsonCall.data["facesBytesWhenPacked"] = faces.itemsize * faces.size + + faceGroupNames = [] + + for fg in mesh.faceGroups: + faceGroupNames.append(fg.name) + + jsonCall.data["faceGroups"] = self.api.mesh.getFaceGroupFaceIndexes() + + coord = mesh.texco + shape = coord.shape + jsonCall.data["numTextureCoords"] = shape[0] + jsonCall.data["textureCoordsTypeCode"] = coord.dtype.str + jsonCall.data["textureCoordsShape"] = shape + jsonCall.data["textureCoordsBytesWhenPacked"] = coord.itemsize * coord.size + + fuvs = mesh.fuvs + shape = fuvs.shape + jsonCall.data["numFaceUVMappings"] = shape[0] + jsonCall.data["faceUVMappingsTypeCode"] = fuvs.dtype.str + jsonCall.data["faceUVMappingsShape"] = shape + jsonCall.data["faceUVMappingsBytesWhenPacked"] = fuvs.itemsize * fuvs.size + + skin = getSkinBlender() + skinColor = skin.getDiffuseColor() + jsonCall.data["skinColor"] = skinColor.asTuple() + (1.0, ) + + + def _boneToHash(self, boneHierarchy, bone, recursionLevel=1): + out = {} + out["name"] = bone.name + out["headPos"] = bone.headPos + out["tailPos"] = bone.tailPos + + restMatrix = bone.matRestGlobal + matrix = np.array((restMatrix[0], -restMatrix[2], restMatrix[1], restMatrix[3])) + qw, qx, qy, qz = quaternion_from_matrix(matrix) + if qw < 1e-4: + roll = 0 + else: + roll = math.pi - 2 * math.atan2(qy, qw) + if roll < -math.pi: + roll += 2 * math.pi + elif roll > math.pi: + roll -= 2 * math.pi + + out["matrix"] = [list(restMatrix[0,:]), list(restMatrix[1,:]), list(restMatrix[2,:]), list(restMatrix[3,:])] + out["roll"] = roll + + out["children"] = [] + boneHierarchy.append(out) + + # Just a security measure. + if recursionLevel < 30: + for child in bone.children: + self._boneToHash(out["children"], child, recursionLevel+1) + + def getSkeleton(self, conn, jsonCall): + + out = {} + skeleton = self.human.getSkeleton() + + boneHierarchy = [] + + yOffset = -1 * self.human.getJointPosition('ground')[1] + out["offset"] = [0.0, 0.0, yOffset] + + if not skeleton is None: + out["name"] = skeleton.name + for bone in skeleton.roots: + self._boneToHash(boneHierarchy, bone) + else: + out["name"] = "none" + + out["bones"] = boneHierarchy + jsonCall.data = out + + def getBodyWeightInfo(self, conn, jsonCall): + + out = {} + weightList = [] + + skeleton = self.human.getSkeleton() + rawWeights = self.human.getVertexWeights(skeleton) + + sumVerts = 0 + sumVertListBytes = 0 + sumWeightsBytes = 0 + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + bw = {} + bw["bone"] = key + bw["numVertices"] = len(rawWeights.data[key][0]) + + verts = rawWeights.data[key][0] + weights = rawWeights.data[key][1] + + bw["vertListBytesWhenPacked"] = verts.itemsize * verts.size + bw["weightsBytesWhenPacked"] = weights.itemsize * weights.size + weightList.append(bw) + + sumVerts = sumVerts + bw["numVertices"] + sumVertListBytes = sumVertListBytes + bw["vertListBytesWhenPacked"] + sumWeightsBytes = sumWeightsBytes + bw["weightsBytesWhenPacked"] + + out["sumVerts"] = sumVerts + out["sumVertListBytes"] = sumVertListBytes + out["sumWeightsBytes"] = sumWeightsBytes + out["weights"] = weightList + + jsonCall.data = out + + def getBodyWeightsVertList(self, conn, jsonCall): + jsonCall.responseIsBinary = True + + skeleton = self.human.getSkeleton() + rawWeights = self.human.getVertexWeights(skeleton) + + allVerts = None + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + + if allVerts is None: + allVerts = rawWeights.data[key][0] + else: + allVerts = np.append(allVerts, rawWeights.data[key][0]) + + jsonCall.data = allVerts.tobytes() + + def getBodyWeights(self, conn, jsonCall): + jsonCall.responseIsBinary = True + + skeleton = self.human.getSkeleton() + rawWeights = self.human.getVertexWeights(skeleton) + + allVerts = None + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + + if allVerts is None: + allVerts = rawWeights.data[key][1] + else: + allVerts = np.append(allVerts, rawWeights.data[key][1]) + + jsonCall.data = allVerts.tobytes() + + def getProxyWeightInfo(self, conn, jsonCall): + + out = {} + weightList = [] + + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + skeleton = self.human.getSkeleton() + + humanWeights = self.human.getVertexWeights(skeleton) + rawWeights = proxy.getVertexWeights(humanWeights, skeleton, allowCache=True) + + #pp.pprint(rawWeights) + #weights = mesh.getVertexWeights(pxySeedWeights) + + sumVerts = 0 + sumVertListBytes = 0 + sumWeightsBytes = 0 + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + bw = {} + bw["bone"] = key + bw["numVertices"] = len(rawWeights.data[key][0]) + + verts = rawWeights.data[key][0] + weights = rawWeights.data[key][1] + + bw["vertListBytesWhenPacked"] = verts.itemsize * verts.size + bw["weightsBytesWhenPacked"] = weights.itemsize * weights.size + weightList.append(bw) + + sumVerts = sumVerts + bw["numVertices"] + sumVertListBytes = sumVertListBytes + bw["vertListBytesWhenPacked"] + sumWeightsBytes = sumWeightsBytes + bw["weightsBytesWhenPacked"] + + out["sumVerts"] = sumVerts + out["sumVertListBytes"] = sumVertListBytes + out["sumWeightsBytes"] = sumWeightsBytes + out["weights"] = weightList + + jsonCall.data = out + + def getProxyWeightsVertList(self, conn, jsonCall): + jsonCall.responseIsBinary = True + + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + skeleton = self.human.getSkeleton() + + humanWeights = self.human.getVertexWeights(skeleton) + + #start = int(round(time.time() * 1000)) + rawWeights = proxy.getVertexWeights(humanWeights, skeleton, allowCache=True) + #stop = int(round(time.time() * 1000)) + #print("Calculating rawWeights for " + proxy.name + " took " + str(stop - start) + " milliseconds") + + allVerts = None + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + + if allVerts is None: + allVerts = rawWeights.data[key][0] + else: + allVerts = np.append(allVerts, rawWeights.data[key][0]) + + jsonCall.data = allVerts.tobytes() + + def getProxyWeights(self, conn, jsonCall): + jsonCall.responseIsBinary = True + + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + skeleton = self.human.getSkeleton() + + humanWeights = self.human.getVertexWeights(skeleton) + + #start = int(round(time.time() * 1000)) + rawWeights = proxy.getVertexWeights(humanWeights, skeleton, allowCache=True) + #stop = int(round(time.time() * 1000)) + #print("Calculating rawWeights for " + proxy.name + " took " + str(stop - start) + " milliseconds") + + allVerts = None + + boneKeys = list(rawWeights.data.keys()) + boneKeys.sort() + + for key in boneKeys: + + if allVerts is None: + allVerts = rawWeights.data[key][1] + else: + allVerts = np.append(allVerts, rawWeights.data[key][1]) + + jsonCall.data = allVerts.tobytes() + + def getPose(self,conn,jsonCall): + + poseFilename = jsonCall.params.get("poseFilename") # use get, since might not be there + + if poseFilename is not None: + filename, file_extension = os.path.splitext(poseFilename) + if file_extension == ".mhpose": + self.api.skeleton.setExpressionFromFile(poseFilename) + if file_extension == ".bvh": + self.api.skeleton.setPoseFromFile(poseFilename) + + self.parent.addMessage("Constructing dict with bone matrices.") + + skeleton = self.human.getSkeleton() + skelobj = dict() + + bones = skeleton.getBones() + + for bone in bones: + rmat = bone.getRestMatrix('zUpFaceNegY') + skelobj[bone.name] = [ list(rmat[0,:]), list(rmat[1,:]), list(rmat[2,:]), list(rmat[3,:]) ] + + jsonCall.data = skelobj + + def _getProxyMesh(self, proxy): + if proxy.type == "Proxymeshes": + if not self.human.proxy is None and not self.human.proxy.name is None: + return self.human._Object__proxyMesh + return proxy.object.getSeedMesh() + + def getProxyVerticesBinary(self,conn,jsonCall): + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + jsonCall.responseIsBinary = True + coord = self._getProxyMesh(proxy).coord + jsonCall.data = coord.tobytes() + + def getProxyFacesBinary(self,conn,jsonCall): + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + jsonCall.responseIsBinary = True + faces = self._getProxyMesh(proxy).fvert + jsonCall.data = faces.tobytes() + + def getProxyMaterialInfo(self,conn,jsonCall): + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + if proxy.type == "Proxymeshes": + if self.human.material.name == 'XrayMaterial' and self.human._backUpMaterial: + material = self.human._backUpMaterial + else: + material = self.human.material + else: + if self.human.material.name == 'XrayMaterial' and proxy._backUpMaterial: + material = proxy._backUpMaterial + else: + material = proxy.object.material + jsonCall.data = self.api.assets.materialToHash(material) + + def getProxyTextureCoordsBinary(self,conn,jsonCall): + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + jsonCall.responseIsBinary = True + texco = self._getProxyMesh(proxy).texco + jsonCall.data = texco.tobytes() + + def getProxyFaceUVMappingsBinary(self,conn,jsonCall): + uuid = jsonCall.params["uuid"] + proxy = self._getProxyByUUID(uuid) + jsonCall.responseIsBinary = True + faces = self._getProxyMesh(proxy).fuvs + jsonCall.data = faces.tobytes() diff --git a/makehuman/plugins/8_server_socket/modops.py b/makehuman/plugins/8_server_socket/modops.py new file mode 100644 index 000000000..200fb0647 --- /dev/null +++ b/makehuman/plugins/8_server_socket/modops.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +from .abstractop import AbstractOp + +class SocketModifierOps(AbstractOp): + + def __init__(self, sockettaskview): + super().__init__(sockettaskview) + self.functions["applyModifier"] = self.applyModifier + self.functions["getAppliedTargets"] = self.getAppliedTargets + self.functions["getAvailableModifierNames"] = self.getAvailableModifierNames + + def getAvailableModifierNames(self,conn,jsonCall): + jsonCall.data = self.api.modifiers.getAvailableModifierNames() + + def getAppliedTargets(self,conn,jsonCall): + jsonCall.data = self.api.modifiers.getAppliedTargets() + + def applyModifier(self,conn,jsonCall): + modifierName = jsonCall.getParam("modifier") + power = float(jsonCall.getParam("power")) + modifier = self.api.internals.getHuman().getModifier(modifierName) + + if not modifier: + jsonCall.setError("No such modifier") + return + + self.api.modifiers.applyModifier(modifierName,power,True) + jsonCall.setData("OK") + + + diff --git a/makehuman/plugins/8_server_socket/workerthread.py b/makehuman/plugins/8_server_socket/workerthread.py new file mode 100644 index 000000000..9ab81747f --- /dev/null +++ b/makehuman/plugins/8_server_socket/workerthread.py @@ -0,0 +1,120 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman server socket plugin + +**Product Home Page:** TBD + +**Code Home Page:** TBD + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin opens a TCP socket and accepts some basic commands. It +does not make much sense without a corresponding client. + +""" + +import gui3d +import mh +import gui +import socket +import json + +from core import G + +mhapi = gui3d.app.mhapi + +from PyQt5 import QtGui +from PyQt5 import QtCore +from PyQt5.QtGui import * +from PyQt5 import QtWidgets +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +qtSignal = QtCore.pyqtSignal +qtSlot = QtCore.pyqtSlot + +QThread = mhapi.ui.QtCore.QThread + +from .dirops import SocketDirOps +from .meshops import SocketMeshOps +from .modops import SocketModifierOps + +class WorkerThread(QThread): + + signalAddMessage = qtSignal(str) + signalEvaluateCall = qtSignal() + + def __init__(self, parent=None, socketConfig=None): + QThread.__init__(self, parent) + self.exiting = False + self.log = mhapi.utility.getLogChannel("socket") + self.socketConfig = {'host' : '127.0.0.1', + 'port' : 12345} + if socketConfig and isinstance(socketConfig, dict): + self.socketConfig['host'] = socketConfig.get('host', '127.0.0.1') + self.socketConfig['port'] = socketConfig.get('port', 12345) + + def addMessage(self,message,newLine = True): + self.signalAddMessage.emit(message) + pass + + def run(self): + self.addMessage("Opening server socket... ") + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + try: + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((self.socketConfig.get('host'), self.socketConfig.get('port'))) + except socket.error as msg: + self.addMessage('Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] + "\n") + return + + self.addMessage("Opened on host {0}\nOpened at port {1}\n".format(self.socketConfig.get('host'), self.socketConfig.get('port'))) + + self.socket.listen(10) + + while not self.exiting: + self.addMessage("Waiting for connection.") + + try: + conn, addr = self.socket.accept() + + if conn and not self.exiting: + self.addMessage("Connected with " + str(addr[0]) + ":" + str(addr[1])) + data = conn.recv(8192) + self.addMessage("Client says: '" + str(data, encoding='utf-8') + "'") + data = gui3d.app.mhapi.internals.JsonCall(data) + + self.jsonCall = data + self.currentConnection = conn + + self.signalEvaluateCall.emit() + except socket.error: + """Assume this is because we closed the socket from outside""" + pass + + def stopListening(self): + if not self.exiting: + self.addMessage("Stopping socket connection") + self.exiting = True + try: + self.socket.shutdown(socket.SHUT_RDWR) + except socket.error: + """If the socket was not connected, shutdown will complain. This isn't a problem, + so just ignore.""" + pass + self.socket.close() + + def __del__(self): + self.stopListening() + + + diff --git a/makehuman/plugins/9_massproduce/__init__.py b/makehuman/plugins/9_massproduce/__init__.py new file mode 100644 index 000000000..8058d5bf2 --- /dev/null +++ b/makehuman/plugins/9_massproduce/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +**Project Name:** MakeHuman community mass produce + +**Product Home Page:** TBD + +**Code Home Page:** TBD + +**Authors:** Joel Palmius + +**Copyright(c):** Joel Palmius 2016 + +**Licensing:** MIT + +Abstract +-------- + +This plugin generates and exports series of characters + +""" + +from .massproduce import MassProduceTaskView + +category = None +mpView = None + +def load(app): + category = app.getCategory('Community') + downloadView = category.addTask(MassProduceTaskView(category)) + +def unload(app): + pass + diff --git a/makehuman/plugins/9_massproduce/humanstate.py b/makehuman/plugins/9_massproduce/humanstate.py new file mode 100644 index 000000000..de63f36c7 --- /dev/null +++ b/makehuman/plugins/9_massproduce/humanstate.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import random +import gui3d +import gui +from core import G +from .modifiergroups import ModifierInfo +import re +import material + +from .randomizeaction import RandomizeAction +from .modifiergroups import MACROGROUPS + +mhapi = gui3d.app.mhapi + +from PyQt5.QtWidgets import * + +import pprint +pp = pprint.PrettyPrinter(indent=4) + +class HumanState(): + + def __init__(self, settings = None): + + self.settings = settings + self.human = G.app.selectedHuman + self.macroModifierValues = dict() + self.appliedTargets = dict(self.human.targetsDetailStack) + + self.skin = material.Material().copyFrom(self.human.material) + self.hair = mhapi.assets.getEquippedHair() + self.eyebrows = mhapi.assets.getEquippedEyebrows() + self.eyelashes = mhapi.assets.getEquippedEyelashes() + self.clothes = mhapi.assets.getEquippedClothes() + + self._fillMacroModifierValues() + + self.modifierInfo = ModifierInfo() + + if not settings is None: + self._randomizeMacros() + if settings.getValue("materials", "randomizeSkinMaterials"): + self._randomizeSkin() + self._randomizeProxies() + self._randomizeDetails() + + def _fillMacroModifierValues(self): + for group in MACROGROUPS.keys(): + for n in MACROGROUPS[group]: + mod = self.human.getModifier(n) + v = mod.getValue() + self.macroModifierValues[n] = v + + def _randomizeOneSidedMaxMin(self, valuesHash, modifierList, maximumValue, minimumValue): + + max = maximumValue + min = minimumValue + + if(min > max): + min = maximumValue + max = minimumValue + + avg = (max - min) / 2.0 + + for name in modifierList: + val = self.getRandomValue(min, max) + valuesHash[name] = val + + def _pickOne(self, valuesHash, modifierList): + for n in modifierList: + valuesHash[n] = 0.0 + num = len(modifierList) + pickedVal = random.randrange(num) + pickedName = modifierList[pickedVal] + valuesHash[pickedName] = 1.0 + + def _pickOneFromArray(self, values): + num = len(values) + pickedVal = random.randrange(num) + return values[pickedVal] + + def _dichotomous(self, valuesHash, modifierList): + for n in modifierList: + valuesHash[n] = float(random.randrange(2)) + + def _randomizeModifierGroup(self, modifierGroup, debug=False): + if debug: + print("RANDOMIZING " + modifierGroup) + mfi = self.modifierInfo.getModifierInfoForGroup(modifierGroup) + maxdev = self.settings.getValue("modeling","maxdev") + for mi in mfi: + print(mi) + modifier = mi["modifier"] + default = float(mi["defaultValue"]) + twosided = mi["twosided"] + + min = default - maxdev + max = default + maxdev + newval = self.getNormalRandomValue(min, max, default) + + if debug: + print("DEFAULT: " + str(default)) + print("MAXDEV: " + str(maxdev)) + print("MIN: " + str(min)) + print("MAX: " + str(max)) + print("NEWVAL: " + str(newval)) + + if newval > 1.0: + newval = 1.0 + + if twosided: + if newval < -1.0: + newval = -1.0 + else: + if newval < 0.0: + newval = 0.0 + + modifier.setValue(newval) + + if debug: + print("SETTING " + str(modifier) + " to " + str(newval)) + + if mi["leftright"]: + sname = modifier.getSymmetricOpposite() + smod = self.human.getModifier(sname) + smod.setValue(newval) + + + def _randomizeDetails(self): + names = self.settings.getNames("modeling") + nondetail = ["maxdev","symmetry"] + for name in names: + if not name in nondetail and self.settings.getValue("modeling",name): + if name == "breast": + self._randomizeBreasts() + else: + self._randomizeModifierGroup(name) + + def _randomizeBreasts(self): + if self._getCurrentGender() == "female": + self._randomizeModifierGroup("breast",False) + + def _randomizeMacros(self): + + if self.settings.getValue("macro","randomizeAge"): + min = self.settings.getValue("macro", "ageMinimum") + max = self.settings.getValue("macro", "ageMaximum") + self._randomizeOneSidedMaxMin(self.macroModifierValues, MACROGROUPS["age"], min, max) + + if self.settings.getValue("macro", "randomizeWeight"): + min = self.settings.getValue("macro", "weightMinimum") + max = self.settings.getValue("macro", "weightMaximum") + self._randomizeOneSidedMaxMin(self.macroModifierValues, MACROGROUPS["weight"], min, max) + + if self.settings.getValue("macro", "randomizeHeight"): + min = self.settings.getValue("macro", "heightMinimum") + max = self.settings.getValue("macro", "heightMaximum") + self._randomizeOneSidedMaxMin(self.macroModifierValues, MACROGROUPS["height"], min, max) + + if self.settings.getValue("macro", "randomizeMuscle"): + min = self.settings.getValue("macro", "muscleMinimum") + max = self.settings.getValue("macro", "muscleMaximum") + self._randomizeOneSidedMaxMin(self.macroModifierValues, MACROGROUPS["muscle"], min, max) + + if self.settings.getValue("macro", "ethnicity"): + if self.settings.getValue("macro", "ethnicityabsolute"): + self._pickOne(self.macroModifierValues, MACROGROUPS["ethnicity"]) + else: + self._randomizeOneSidedMaxMin(self.macroModifierValues, MACROGROUPS["ethnicity"], 0.0, 1.0) + + if self.settings.getValue("macro", "gender"): + key = MACROGROUPS["gender"][0] + if self.settings.getValue("macro", "genderabsolute"): + self.macroModifierValues[key] = float(random.randrange(2)) + else: + self.macroModifierValues[key] = random.random() + + def _getCurrentEthnicity(self): + for ethn in MACROGROUPS["ethnicity"]: + value = self.macroModifierValues[ethn] + name = ethn.split("/")[1].lower() + if value > 0.9: + return name + return "mixed" + + def _getCurrentGender(self): + key = MACROGROUPS["gender"][0] + value = self.macroModifierValues[key] + gender = "mixed" + + if value < 0.3: + gender = "female" + if value > 0.7: + gender = "male" + + return gender + + def _findSkinForEthnicityAndGender(self,ethnicity,gender): + + skinHash = None + if gender == "female": + category = "allowedFemaleSkins" + else: + category = "allowedMaleSkins" + + matchingSkins = [] + for name in self.settings.getNames(category): + skin = self.settings.getValueHash(category, name) + if skin[ethnicity]: + matchingSkins.append(skin["fullPath"]) + + pick = random.randrange(len(matchingSkins)) + self.skin = material.fromFile(matchingSkins[pick]) + + def _randomizeSkin(self): + + gender = self._getCurrentGender() + ethnicity = self._getCurrentEthnicity() + + self._findSkinForEthnicityAndGender(ethnicity,gender) + + def _findHairForGender(self, gender): + + hairNames = self.settings.getNames("allowedHair") + allowedHair = [] + for hairName in hairNames: + allowed = self.settings.getValue("allowedHair", hairName, gender) + if allowed: + allowedHair.append(hairName) + + pick = random.randrange(len(allowedHair)) + return self.settings.getValue("allowedHair",allowedHair[pick],"fullPath") + + def _randomizeHair(self): + gender = self._getCurrentGender() + fullPath = self._findHairForGender(gender) + self.hair = fullPath + + def _findEyebrowsForGender(self, gender): + + eyebrowsNames = self.settings.getNames("allowedEyebrows") + allowedEyebrows = [] + for eyebrowsName in eyebrowsNames: + allowed = self.settings.getValue("allowedEyebrows", eyebrowsName, gender) + if allowed: + allowedEyebrows.append(eyebrowsName) + + pick = random.randrange(len(allowedEyebrows)) + return self.settings.getValue("allowedEyebrows",allowedEyebrows[pick],"fullPath") + + def _randomizeEyebrows(self): + gender = self._getCurrentGender() + fullPath = self._findEyebrowsForGender(gender) + self.eyebrows = fullPath + + + def _findEyelashesForGender(self, gender): + + eyelashesNames = self.settings.getNames("allowedEyelashes") + allowedEyelashes = [] + for eyelashesName in eyelashesNames: + allowed = self.settings.getValue("allowedEyelashes", eyelashesName, gender) + if allowed: + allowedEyelashes.append(eyelashesName) + + pick = random.randrange(len(allowedEyelashes)) + return self.settings.getValue("allowedEyelashes",allowedEyelashes[pick],"fullPath") + + def _randomizeEyelashes(self): + gender = self._getCurrentGender() + fullPath = self._findEyelashesForGender(gender) + self.eyelashes = fullPath + + def _getAllowedClothesForPartAndGender(self,part,gender): + + gender = gender.lower() + part = part.lower().capitalize() + + if "shoes" not in part.lower(): + part = part + "Clothes" + + setName = "allowed" + part + allNames = list(self.settings.getNames(setName)) + + allowedPaths = [] + for name in allNames: + if self.settings.getValue(setName,name,gender): + allowedPaths.append(self.settings.getValue(setName,name,"fullPath")) + return allowedPaths + + def _randomizeFullClothes(self): + allowed = self._getAllowedClothesForPartAndGender("full", self._getCurrentGender()) + picked = self._pickOneFromArray(allowed) + self.clothes.append(picked) + + def _randomizeUpperClothes(self): + allowed = self._getAllowedClothesForPartAndGender("upper", self._getCurrentGender()) + picked = self._pickOneFromArray(allowed) + self.clothes.append(picked) + + def _randomizeLowerClothes(self): + allowed = self._getAllowedClothesForPartAndGender("lower", self._getCurrentGender()) + picked = self._pickOneFromArray(allowed) + self.clothes.append(picked) + + def _randomizeShoes(self): + allowed = self._getAllowedClothesForPartAndGender("shoes", self._getCurrentGender()) + picked = self._pickOneFromArray(allowed) + self.clothes.append(picked) + + def _randomizeProxies(self): + if self.settings.getValue("proxies","hair"): + self._randomizeHair() + if self.settings.getValue("proxies","eyebrows"): + self._randomizeEyebrows() + if self.settings.getValue("proxies","eyelashes"): + self._randomizeEyelashes() + + for k in ["fullClothes","upperClothes","lowerClothes","shoes"]: + if self.settings.getValue("proxies",k): + self.clothes = [] + + if self.settings.getValue("proxies","fullClothes"): + self._randomizeFullClothes() + + if self.settings.getValue("proxies","upperClothes"): + self._randomizeUpperClothes() + + if self.settings.getValue("proxies","lowerClothes"): + self._randomizeLowerClothes() + + if self.settings.getValue("proxies","shoes"): + self._randomizeShoes() + + def equipClothes(self): + mhapi.assets.unequipAllClothes() + for c in self.clothes: + mhapi.assets.equipClothes(c) + + def applyState(self, assumeBodyReset=False): + + self._applyMacroModifiers() + if assumeBodyReset: + self.human.targetsDetailStack = self.appliedTargets + self.human.material = self.skin + mhapi.assets.equipHair(self.hair) + mhapi.assets.equipEyebrows(self.eyebrows) + mhapi.assets.equipEyelashes(self.eyelashes) + + mhapi.modifiers._threadSafeApplyAllTargets() + + self.equipClothes() + + def _applyMacroModifiers(self): + for group in MACROGROUPS.keys(): + for n in MACROGROUPS[group]: + mod = self.human.getModifier(n) + v = self.macroModifierValues[n] + mod.setValue(v) + + def getRandomValue(self, minValue, maxValue): + size = maxValue - minValue + val = random.random() * size + return minValue + val + + def getNormalRandomValue(self, minValue, maxValue, middleValue, sigmaFactor=0.2): + rangeWidth = float(abs(maxValue - minValue)) + sigma = sigmaFactor * rangeWidth + randomVal = random.gauss(middleValue, sigma) + if randomVal < minValue: + randomVal = minValue + abs(randomVal - minValue) + elif randomVal > maxValue: + randomVal = maxValue - abs(randomVal - maxValue) + return max(minValue, min(randomVal, maxValue)) + + diff --git a/makehuman/plugins/9_massproduce/massproduce.py b/makehuman/plugins/9_massproduce/massproduce.py new file mode 100644 index 000000000..90f1f56f6 --- /dev/null +++ b/makehuman/plugins/9_massproduce/massproduce.py @@ -0,0 +1,864 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import random +import gui3d +import gui +from core import G +from progress import Progress +from .randomizeaction import RandomizeAction +from .randomizationsettings import RandomizationSettings +from .humanstate import HumanState +from .modifiergroups import ModifierInfo + +mhapi = gui3d.app.mhapi + +import mh, time, os, re + +from PyQt5.QtWidgets import * + +import pprint +pp = pprint.PrettyPrinter(indent=4) + +DEFAULT_TABLE_HEIGHT=250 +DEFAULT_LABEL_COLUMN_WIDTH=300 + +class MassProduceTaskView(gui3d.TaskView): + + def __init__(self, category): + gui3d.TaskView.__init__(self, category, 'Mass produce') + + self.human = G.app.selectedHuman + + self.log = mhapi.utility.getLogChannel("massproduce") + + self.randomizationSettings = RandomizationSettings() + + self._setupLeftPanel(self.randomizationSettings) + self._setupMainPanel(self.randomizationSettings) + self._setupRightPanel(self.randomizationSettings) + + def _setupLeftPanel(self, r): + self.addLeftWidget( self._createMacroSettings(r) ) + self.addLeftWidget(mhapi.ui.createLabel()) + self.addLeftWidget( self._createModelingSettings(r) ) + + def _setupMainPanel(self, r): + + self.mainSettingsPanel = QWidget() + self.mainSettingsLayout = QVBoxLayout() + + self._setupRandomizeProxies(self.mainSettingsLayout, r) + self._setupRandomizeMaterials(self.mainSettingsLayout, r) + self._setupAllowedSkinsTables(self.mainSettingsLayout, r) + self._setupAllowedHairTable(self.mainSettingsLayout, r) + self._setupAllowedEyebrowsTable(self.mainSettingsLayout, r) + self._setupAllowedEyelashesTable(self.mainSettingsLayout, r) + self._setupClothesTables(self.mainSettingsLayout, r) + + self.mainSettingsLayout.addStretch() + self.mainSettingsPanel.setLayout(self.mainSettingsLayout) + + self.mainScroll = QScrollArea() + self.mainScroll.setWidget(self.mainSettingsPanel) + self.mainScroll.setWidgetResizable(True) + + self.addTopWidget(self.mainScroll) + + def _setupRightPanel(self, r): + self.addRightWidget(self._createExportSettings(r)) + self.addRightWidget(mhapi.ui.createLabel()) + self.addRightWidget(self._createProducePanel(r)) + + def _setupRandomizeMaterials(self, layout, r): + layout.addWidget(mhapi.ui.createLabel("Randomize materials:")) + layout.addWidget(r.addUI("materials", "randomizeSkinMaterials", mhapi.ui.createCheckBox(label="Randomize skins", selected=True))) + #layout.addWidget(r.addUI("materials", "randomizeHairMaterials", mhapi.ui.createCheckBox(label="Randomize hair material", selected=True))) + #layout.addWidget(r.addUI("materials", "randomizeClothesMaterials", mhapi.ui.createCheckBox(label="Randomize clothes material", selected=True))) + layout.addWidget(mhapi.ui.createLabel()) + + def _setupRandomizeProxies(self, layout, r): + layout.addWidget(mhapi.ui.createLabel("Randomize clothes and body parts:")) + layout.addWidget(r.addUI("proxies", "hair", mhapi.ui.createCheckBox(label="Randomize hair", selected=True))) + layout.addWidget(r.addUI("proxies", "eyelashes", mhapi.ui.createCheckBox(label="Randomize eyelashes", selected=True))) + layout.addWidget(r.addUI("proxies", "eyebrows", mhapi.ui.createCheckBox(label="Randomize eyebrows", selected=True))) + layout.addWidget(r.addUI("proxies", "fullClothes", mhapi.ui.createCheckBox(label="Randomize full body clothes", selected=True))) + layout.addWidget(r.addUI("proxies", "upperClothes", mhapi.ui.createCheckBox(label="Randomize upper body clothes", selected=False))) + layout.addWidget(r.addUI("proxies", "lowerClothes", mhapi.ui.createCheckBox(label="Randomize lower body clothes", selected=False))) + layout.addWidget(r.addUI("proxies", "shoes", mhapi.ui.createCheckBox(label="Randomize shoes", selected=True))) + layout.addWidget(mhapi.ui.createLabel()) + + def _generalMainTableSettings(self, table): + table.setColumnWidth(0, DEFAULT_LABEL_COLUMN_WIDTH) + table.setMinimumHeight(DEFAULT_TABLE_HEIGHT - 50) + table.setMaximumHeight(DEFAULT_TABLE_HEIGHT) + + def _setupClothesTables(self, layout, r): + sysClothes = mhapi.assets.getAvailableSystemClothes() + userClothes = mhapi.assets.getAvailableUserClothes() + + allClothes = [] + allClothes.extend(sysClothes) + allClothes.extend(userClothes) + + femaleFullExplicit = ["female elegantsuit01", + "female casualsuit02", + "female casualsuit01", + "female sportsuit01"] + + maleFullExplicit = ["male worksuit01", + "male elegantsuit01", + "male casualsuit06", + "male casualsuit05", + "male casualsuit04", + "male casualsuit02", + "male casualsuit01", + "male casualsuit03"] + + femaleFullKeyword = ["gown","dress","swimsuit"] + maleFullKeyword = ["wetsuit"] + + femaleUpperExplicit = [] + maleUpperExplicit = [] + femaleUpperKeyword = ["bra","top","shirt","sweater"] + maleUpperKeyword = ["top","shirt","sweater"] + + femaleLowerExplicit = [] + maleLowerExplicit = [] + femaleLowerKeyword = ["skirt","jeans","string","shorts","pants"] + maleLowerKeyword = ["jeans","shorts","pants","trunk"] + + femaleShoesExplicit = [] + maleShoesExplicit = [] + femaleShoesKeyword = ["boot","shoe","sneaker","sock"] + maleShoesKeyword = ["boot","shoe","sneaker","sock"] + + clothesInfos = dict() + allNames = [] + for fullPath in allClothes: + bn = os.path.basename(fullPath).lower() + bn = re.sub(r'.mhclo', '', bn) + bn = re.sub(r'.mhpxy', '', bn) + bn = re.sub(r'_', ' ', bn) + bn = bn.strip() + + name = bn + + clothesInfo = dict() + clothesInfo["fullPath"] = fullPath + clothesInfo["name"] = name + + clothesInfo["maleFull"] = False + clothesInfo["femaleFull"] = False + + clothesInfo["maleUpper"] = False + clothesInfo["femaleUpper"] = False + + clothesInfo["maleLower"] = False + clothesInfo["femaleLower"] = False + + clothesInfo["maleShoes"] = False + clothesInfo["femaleShoes"] = False + + clothesInfo["mixedFull"] = False + clothesInfo["mixedUpper"] = False + clothesInfo["mixedLower"] = False + clothesInfo["mixedShoes"] = False + + if not "female" in name: + if name in maleFullExplicit: + clothesInfo["maleFull"] = True + for k in maleFullKeyword: + if k in name: + clothesInfo["maleFull"] = True + + if name in maleUpperExplicit: + clothesInfo["maleUpper"] = True + for k in maleUpperKeyword: + if k in name: + clothesInfo["maleUpper"] = True + + if name in maleLowerExplicit: + clothesInfo["maleLower"] = True + for k in maleLowerKeyword: + if k in name: + clothesInfo["maleLower"] = True + + if name in maleShoesExplicit: + clothesInfo["maleShoes"] = True + for k in maleShoesKeyword: + if k in name: + clothesInfo["maleShoes"] = True + + if name in femaleFullExplicit: + clothesInfo["femaleFull"] = True + for k in femaleFullKeyword: + if k in name: + clothesInfo["femaleFull"] = True + + if name in femaleUpperExplicit: + clothesInfo["femaleUpper"] = True + for k in femaleUpperKeyword: + if k in name: + clothesInfo["femaleUpper"] = True + + if name in femaleLowerExplicit: + clothesInfo["femaleLower"] = True + for k in femaleLowerKeyword: + if k in name: + clothesInfo["femaleLower"] = True + + if name in femaleShoesExplicit: + clothesInfo["femaleShoes"] = True + for k in femaleShoesKeyword: + if k in name: + clothesInfo["femaleShoes"] = True + + clothesInfo["mixedFull"] = clothesInfo["femaleFull"] or clothesInfo["maleFull"] + clothesInfo["mixedUpper"] = clothesInfo["femaleUpper"] or clothesInfo["maleUpper"] + clothesInfo["mixedLower"] = clothesInfo["femaleLower"] or clothesInfo["maleLower"] + clothesInfo["mixedShoes"] = clothesInfo["femaleShoes"] or clothesInfo["maleShoes"] + + clothesInfos[name] = clothesInfo + allNames.append(name) + + allNames.sort() + + # part = ["Full","Upper","Lower","Shoes"] + # gender = ["male","female"] + # with open("/tmp/table.html","w") as f: + # f.write("\n\n\n") + # f.write("") + # + # for p in part: + # for g in gender: + # f.write("") + # f.write("\n") + # for name in allNames: + # f.write("") + # i = clothesInfos[name] + # f.write("") + # for p in part: + # for g in gender: + # if i[g+p]: + # f.write("") + # else: + # f.write("") + # f.write("\n") + # + # f.write("") + + + self.allowedFullTable = QTableWidget() + self.allowedFullTable.setRowCount(len(allNames)) + self.allowedFullTable.setColumnCount(4) + self.allowedFullTable.setHorizontalHeaderLabels(["Clothes", "Mixed", "Female", "Male"]) + + self.allowedUpperTable = QTableWidget() + self.allowedUpperTable.setRowCount(len(allNames)) + self.allowedUpperTable.setColumnCount(4) + self.allowedUpperTable.setHorizontalHeaderLabels(["Clothes", "Mixed", "Female", "Male"]) + + self.allowedLowerTable = QTableWidget() + self.allowedLowerTable.setRowCount(len(allNames)) + self.allowedLowerTable.setColumnCount(4) + self.allowedLowerTable.setHorizontalHeaderLabels(["Clothes", "Mixed", "Female", "Male"]) + + self.allowedShoesTable = QTableWidget() + self.allowedShoesTable.setRowCount(len(allNames)) + self.allowedShoesTable.setColumnCount(4) + self.allowedShoesTable.setHorizontalHeaderLabels(["Clothes", "Mixed", "Female", "Male"]) + + i = 0 + for name in allNames: + + info = clothesInfos[name] + + self.allowedFullTable.setItem(i, 0, QTableWidgetItem(name)) + self.allowedUpperTable.setItem(i, 0, QTableWidgetItem(name)) + self.allowedLowerTable.setItem(i, 0, QTableWidgetItem(name)) + self.allowedShoesTable.setItem(i, 0, QTableWidgetItem(name)) + + miF = r.addUI("allowedFullClothes", name, mhapi.ui.createCheckBox(""), subName="mixed") + maF = r.addUI("allowedFullClothes", name, mhapi.ui.createCheckBox(""), subName="male") + feF = r.addUI("allowedFullClothes", name, mhapi.ui.createCheckBox(""), subName="female") + + miU = r.addUI("allowedUpperClothes", name, mhapi.ui.createCheckBox(""), subName="mixed") + maU = r.addUI("allowedUpperClothes", name, mhapi.ui.createCheckBox(""), subName="male") + feU = r.addUI("allowedUpperClothes", name, mhapi.ui.createCheckBox(""), subName="female") + + miL = r.addUI("allowedLowerClothes", name, mhapi.ui.createCheckBox(""), subName="mixed") + maL = r.addUI("allowedLowerClothes", name, mhapi.ui.createCheckBox(""), subName="male") + feL = r.addUI("allowedLowerClothes", name, mhapi.ui.createCheckBox(""), subName="female") + + miS = r.addUI("allowedShoes", name, mhapi.ui.createCheckBox(""), subName="mixed") + maS = r.addUI("allowedShoes", name, mhapi.ui.createCheckBox(""), subName="male") + feS = r.addUI("allowedShoes", name, mhapi.ui.createCheckBox(""), subName="female") + + self.allowedFullTable.setCellWidget(i, 1, miF) + self.allowedFullTable.setCellWidget(i, 2, feF) + self.allowedFullTable.setCellWidget(i, 3, maF) + + self.allowedUpperTable.setCellWidget(i, 1, miU) + self.allowedUpperTable.setCellWidget(i, 2, feU) + self.allowedUpperTable.setCellWidget(i, 3, maU) + + self.allowedLowerTable.setCellWidget(i, 1, miL) + self.allowedLowerTable.setCellWidget(i, 2, feL) + self.allowedLowerTable.setCellWidget(i, 3, maL) + + self.allowedShoesTable.setCellWidget(i, 1, miS) + self.allowedShoesTable.setCellWidget(i, 2, feS) + self.allowedShoesTable.setCellWidget(i, 3, maS) + + r.addUI("allowedFullClothes", name, info["fullPath"], subName="fullPath") + r.addUI("allowedUpperClothes", name, info["fullPath"], subName="fullPath") + r.addUI("allowedLowerClothes", name, info["fullPath"], subName="fullPath") + r.addUI("allowedShoes", name, info["fullPath"], subName="fullPath") + + miF.setChecked(info["mixedFull"]) + maF.setChecked(info["maleFull"]) + feF.setChecked(info["femaleFull"]) + + miU.setChecked(info["mixedUpper"]) + maU.setChecked(info["maleUpper"]) + feU.setChecked(info["femaleUpper"]) + + miL.setChecked(info["mixedLower"]) + maL.setChecked(info["maleLower"]) + feL.setChecked(info["femaleLower"]) + + miS.setChecked(info["mixedShoes"]) + maS.setChecked(info["maleShoes"]) + feS.setChecked(info["femaleShoes"]) + + i = i + 1 + + self._generalMainTableSettings(self.allowedFullTable) + self._generalMainTableSettings(self.allowedUpperTable) + self._generalMainTableSettings(self.allowedLowerTable) + self._generalMainTableSettings(self.allowedShoesTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed full body clothes:")) + layout.addWidget(self.allowedFullTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed upper body clothes:")) + layout.addWidget(self.allowedUpperTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed lower body clothes:")) + layout.addWidget(self.allowedLowerTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed shoes:")) + layout.addWidget(self.allowedShoesTable) + + def _setupAllowedHairTable(self, layout, r): + sysHair = mhapi.assets.getAvailableSystemHair() + userHair = mhapi.assets.getAvailableUserHair() + + hair = [] + hair.extend(sysHair) + hair.extend(userHair) + + femaleOnly = [ + "bob01", + "bob02", + "long01", + "braid01", + "ponytail01" + ] + + maleOnly = [ + "short02", + "short04" + ] + + hairInfo = dict() + + for fullPath in hair: + bn = os.path.basename(fullPath).lower() + bn = re.sub(r'.mhclo', '', bn) + bn = re.sub(r'.mhpxy', '', bn) + bn = re.sub(r'_', ' ', bn) + bn = bn.strip() + + hairName = bn + + if not hairName in hairInfo: + hairInfo[hairName] = dict() + hairInfo[hairName]["fullPath"] = fullPath + + allowMixed = True + allowFemale = True + allowMale = True + + if hairName in femaleOnly: + allowMale = False + + if hairName in maleOnly: + allowFemale = False + + hairInfo[hairName]["allowMixed"] = allowMixed + hairInfo[hairName]["allowFemale"] = allowFemale + hairInfo[hairName]["allowMale"] = allowMale + + hairNames = list(hairInfo.keys()) + hairNames.sort() + + self.allowedHairTable = QTableWidget() + self.allowedHairTable.setRowCount(len(hairNames)) + self.allowedHairTable.setColumnCount(4) + self.allowedHairTable.setHorizontalHeaderLabels(["Hair", "Mixed", "Female", "Male"]) + + i = 0 + for hairName in hairNames: + hairSettings = hairInfo[hairName] + hairWidgets = dict() + + self.allowedHairTable.setItem(i, 0, QTableWidgetItem(hairName)) + hairWidgets["mixed"] = r.addUI("allowedHair", hairName, mhapi.ui.createCheckBox(""), subName="mixed") + hairWidgets["female"] = r.addUI("allowedHair", hairName, mhapi.ui.createCheckBox(""), subName="female") + hairWidgets["male"] = r.addUI("allowedHair", hairName, mhapi.ui.createCheckBox(""), subName="male") + r.addUI("allowedHair", hairName, hairSettings["fullPath"], subName="fullPath") + + self.allowedHairTable.setCellWidget(i, 1, hairWidgets["mixed"]) + self.allowedHairTable.setCellWidget(i, 2, hairWidgets["female"]) + self.allowedHairTable.setCellWidget(i, 3, hairWidgets["male"]) + + hairWidgets["mixed"].setChecked(hairSettings["allowMixed"]) + hairWidgets["female"].setChecked(hairSettings["allowFemale"]) + hairWidgets["male"].setChecked(hairSettings["allowMale"]) + + i = i + 1 + + self._generalMainTableSettings(self.allowedHairTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed hair:")) + layout.addWidget(self.allowedHairTable) + + def _setupAllowedEyebrowsTable(self, layout, r): + sysEyebrows = mhapi.assets.getAvailableSystemEyebrows() + userEyebrows = mhapi.assets.getAvailableUserEyebrows() + + eyebrows = [] + eyebrows.extend(sysEyebrows) + eyebrows.extend(userEyebrows) + + eyebrowsInfo = dict() + + for fullPath in eyebrows: + bn = os.path.basename(fullPath).lower() + bn = re.sub(r'.mhclo', '', bn) + bn = re.sub(r'.mhpxy', '', bn) + bn = re.sub(r'_', ' ', bn) + bn = bn.strip() + + eyebrowsName = bn + + if not eyebrowsName in eyebrowsInfo: + eyebrowsInfo[eyebrowsName] = dict() + eyebrowsInfo[eyebrowsName]["fullPath"] = fullPath + + allowMixed = True + allowFemale = True + allowMale = True + + # TODO: Check if any eyebrows look gender specific + + eyebrowsInfo[eyebrowsName]["allowMixed"] = allowMixed + eyebrowsInfo[eyebrowsName]["allowFemale"] = allowFemale + eyebrowsInfo[eyebrowsName]["allowMale"] = allowMale + + eyebrowsNames = list(eyebrowsInfo.keys()) + eyebrowsNames.sort() + + self.allowedEyebrowsTable = QTableWidget() + self.allowedEyebrowsTable.setRowCount(len(eyebrowsNames)) + self.allowedEyebrowsTable.setColumnCount(4) + self.allowedEyebrowsTable.setHorizontalHeaderLabels(["Eyebrows", "Mixed", "Female", "Male"]) + + i = 0 + for eyebrowsName in eyebrowsNames: + eyebrowsSettings = eyebrowsInfo[eyebrowsName] + eyebrowsWidgets = dict() + + self.allowedEyebrowsTable.setItem(i, 0, QTableWidgetItem(eyebrowsName)) + eyebrowsWidgets["mixed"] = r.addUI("allowedEyebrows", eyebrowsName, mhapi.ui.createCheckBox(""), subName="mixed") + eyebrowsWidgets["female"] = r.addUI("allowedEyebrows", eyebrowsName, mhapi.ui.createCheckBox(""), subName="female") + eyebrowsWidgets["male"] = r.addUI("allowedEyebrows", eyebrowsName, mhapi.ui.createCheckBox(""), subName="male") + r.addUI("allowedEyebrows", eyebrowsName, eyebrowsSettings["fullPath"], subName="fullPath") + + self.allowedEyebrowsTable.setCellWidget(i, 1, eyebrowsWidgets["mixed"]) + self.allowedEyebrowsTable.setCellWidget(i, 2, eyebrowsWidgets["female"]) + self.allowedEyebrowsTable.setCellWidget(i, 3, eyebrowsWidgets["male"]) + + eyebrowsWidgets["mixed"].setChecked(eyebrowsSettings["allowMixed"]) + eyebrowsWidgets["female"].setChecked(eyebrowsSettings["allowFemale"]) + eyebrowsWidgets["male"].setChecked(eyebrowsSettings["allowMale"]) + + i = i + 1 + + self._generalMainTableSettings(self.allowedEyebrowsTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed eyebrows:")) + layout.addWidget(self.allowedEyebrowsTable) + + def _setupAllowedEyelashesTable(self, layout, r): + sysEyelashes = mhapi.assets.getAvailableSystemEyelashes() + userEyelashes = mhapi.assets.getAvailableUserEyelashes() + + eyelashes = [] + eyelashes.extend(sysEyelashes) + eyelashes.extend(userEyelashes) + + eyelashesInfo = dict() + + for fullPath in eyelashes: + bn = os.path.basename(fullPath).lower() + bn = re.sub(r'.mhclo', '', bn) + bn = re.sub(r'.mhpxy', '', bn) + bn = re.sub(r'_', ' ', bn) + bn = bn.strip() + + eyelashesName = bn + + if not eyelashesName in eyelashesInfo: + eyelashesInfo[eyelashesName] = dict() + eyelashesInfo[eyelashesName]["fullPath"] = fullPath + + allowMixed = True + allowFemale = True + allowMale = True + + # TODO: Check if any eyelashes look gender specific + + eyelashesInfo[eyelashesName]["allowMixed"] = allowMixed + eyelashesInfo[eyelashesName]["allowFemale"] = allowFemale + eyelashesInfo[eyelashesName]["allowMale"] = allowMale + + eyelashesNames = list(eyelashesInfo.keys()) + eyelashesNames.sort() + + self.allowedEyelashesTable = QTableWidget() + self.allowedEyelashesTable.setRowCount(len(eyelashesNames)) + self.allowedEyelashesTable.setColumnCount(4) + self.allowedEyelashesTable.setHorizontalHeaderLabels(["Eyelashes", "Mixed", "Female", "Male"]) + + i = 0 + for eyelashesName in eyelashesNames: + eyelashesSettings = eyelashesInfo[eyelashesName] + eyelashesWidgets = dict() + + self.allowedEyelashesTable.setItem(i, 0, QTableWidgetItem(eyelashesName)) + eyelashesWidgets["mixed"] = r.addUI("allowedEyelashes", eyelashesName, mhapi.ui.createCheckBox(""), subName="mixed") + eyelashesWidgets["female"] = r.addUI("allowedEyelashes", eyelashesName, mhapi.ui.createCheckBox(""), subName="female") + eyelashesWidgets["male"] = r.addUI("allowedEyelashes", eyelashesName, mhapi.ui.createCheckBox(""), subName="male") + r.addUI("allowedEyelashes", eyelashesName, eyelashesSettings["fullPath"], subName="fullPath") + + self.allowedEyelashesTable.setCellWidget(i, 1, eyelashesWidgets["mixed"]) + self.allowedEyelashesTable.setCellWidget(i, 2, eyelashesWidgets["female"]) + self.allowedEyelashesTable.setCellWidget(i, 3, eyelashesWidgets["male"]) + + eyelashesWidgets["mixed"].setChecked(eyelashesSettings["allowMixed"]) + eyelashesWidgets["female"].setChecked(eyelashesSettings["allowFemale"]) + eyelashesWidgets["male"].setChecked(eyelashesSettings["allowMale"]) + + i = i + 1 + + self._generalMainTableSettings(self.allowedEyelashesTable) + + layout.addWidget(mhapi.ui.createLabel("")) + layout.addWidget(mhapi.ui.createLabel("Allowed eyelashes:")) + layout.addWidget(self.allowedEyelashesTable) + + + def _setupAllowedSkinsTables(self, layout, r): + + sysSkins = mhapi.assets.getAvailableSystemSkins() + userSkins = mhapi.assets.getAvailableUserSkins() + + allowedFemaleSkins = dict() + allowedMaleSkins = dict() + + #pp.pprint(sysSkins) + #pp.pprint(userSkins) + + skinBaseNames = [] + for s in sysSkins: + bn = os.path.basename(s).lower() + bn = re.sub(r'.mhmat','',bn) + bn = re.sub(r'_', ' ', bn) + skinBaseNames.append(bn) + + allowedFemaleSkins[bn] = dict() + allowedFemaleSkins[bn]["fullPath"] = os.path.abspath(s) + allowedMaleSkins[bn] = dict() + allowedMaleSkins[bn]["fullPath"] = os.path.abspath(s) + + for s in userSkins: + bn = os.path.basename(s).lower() + bn = re.sub(r'.mhmat', '', bn) + bn = re.sub(r'_', ' ', bn) + skinBaseNames.append(bn) + + allowedFemaleSkins[bn] = dict() + allowedFemaleSkins[bn]["fullPath"] = os.path.abspath(s) + allowedMaleSkins[bn] = dict() + allowedMaleSkins[bn]["fullPath"] = os.path.abspath(s) + + skinBaseNames.sort() + + self.allowedFemaleSkinsTable = QTableWidget() + self.allowedFemaleSkinsTable.setRowCount(len(skinBaseNames)) + self.allowedFemaleSkinsTable.setColumnCount(5) + self.allowedFemaleSkinsTable.setHorizontalHeaderLabels(["Skin", "Mixed", "African", "Asian", "Caucasian"]) + + + self.allowedMaleSkinsTable = QTableWidget() + self.allowedMaleSkinsTable.setRowCount(len(skinBaseNames)) + self.allowedMaleSkinsTable.setColumnCount(5) + self.allowedMaleSkinsTable.setHorizontalHeaderLabels(["Skin", "Mixed", "African", "Asian", "Caucasian"]) + + skins = dict() + + i = 0 + for n in skinBaseNames: + + female = allowedFemaleSkins[n] + male = allowedMaleSkins[n] + + self.allowedFemaleSkinsTable.setItem(i, 0, QTableWidgetItem(n)) + self.allowedMaleSkinsTable.setItem(i, 0, QTableWidgetItem(n)) + + male["mixed"] = r.addUI("allowedMaleSkins",n,mhapi.ui.createCheckBox(""),subName="mixed") + male["african"] = r.addUI("allowedMaleSkins",n,mhapi.ui.createCheckBox(""),subName="african") + male["asian"] = r.addUI("allowedMaleSkins",n,mhapi.ui.createCheckBox(""),subName="asian") + male["caucasian"] = r.addUI("allowedMaleSkins",n,mhapi.ui.createCheckBox(""),subName="caucasian") + r.addUI("allowedMaleSkins", n, allowedMaleSkins[n]["fullPath"], subName="fullPath") + + self.allowedMaleSkinsTable.setCellWidget(i, 1, male["mixed"]) + self.allowedMaleSkinsTable.setCellWidget(i, 2, male["african"]) + self.allowedMaleSkinsTable.setCellWidget(i, 3, male["asian"]) + self.allowedMaleSkinsTable.setCellWidget(i, 4, male["caucasian"]) + + female["mixed"] = r.addUI("allowedFemaleSkins",n,mhapi.ui.createCheckBox(""),subName="mixed") + female["african"] = r.addUI("allowedFemaleSkins",n,mhapi.ui.createCheckBox(""),subName="african") + female["asian"] = r.addUI("allowedFemaleSkins",n,mhapi.ui.createCheckBox(""),subName="asian") + female["caucasian"] = r.addUI("allowedFemaleSkins",n,mhapi.ui.createCheckBox(""),subName="caucasian") + r.addUI("allowedFemaleSkins", n, allowedFemaleSkins[n]["fullPath"], subName="fullPath") + + self.allowedFemaleSkinsTable.setCellWidget(i, 1, female["mixed"]) + self.allowedFemaleSkinsTable.setCellWidget(i, 2, female["african"]) + self.allowedFemaleSkinsTable.setCellWidget(i, 3, female["asian"]) + self.allowedFemaleSkinsTable.setCellWidget(i, 4, female["caucasian"]) + + if self._matchesEthnicGender(n,"female") and not "special" in n: + + female["mixed"].setChecked(True) + + if self._matchesEthnicGender(n,ethnicity="african"): + female["african"].setChecked(True) + if self._matchesEthnicGender(n,ethnicity="asian") and not self._matchesEthnicGender(n,ethnicity="caucasian"): + female["asian"].setChecked(True) + if self._matchesEthnicGender(n,ethnicity="caucasian"): + female["caucasian"].setChecked(True) + + if self._matchesEthnicGender(n,"male") and not self._matchesEthnicGender(n,"female") and not "special" in n: + + male["mixed"].setChecked(True) + + if self._matchesEthnicGender(n,ethnicity="african"): + male["african"].setChecked(True) + if self._matchesEthnicGender(n,ethnicity="asian") and not self._matchesEthnicGender(n,ethnicity="caucasian"): + male["asian"].setChecked(True) + if self._matchesEthnicGender(n,ethnicity="caucasian"): + male["caucasian"].setChecked(True) + + + i = i + 1 + + self._generalMainTableSettings(self.allowedFemaleSkinsTable) + self._generalMainTableSettings(self.allowedMaleSkinsTable) + + i = 1 + while i < 5: + self.allowedFemaleSkinsTable.setColumnWidth(i, 80) + self.allowedMaleSkinsTable.setColumnWidth(i, 80) + i = i + 1 + + + layout.addWidget(mhapi.ui.createLabel("Allowed female skins:")) + layout.addWidget(self.allowedFemaleSkinsTable) + + layout.addWidget(mhapi.ui.createLabel("")) + + layout.addWidget(mhapi.ui.createLabel("Allowed male skins:")) + layout.addWidget(self.allowedMaleSkinsTable) + + self.allowedFemaleSkins = allowedFemaleSkins + self.allowedMaleSkins = allowedMaleSkins + + + def _matchesEthnicGender(self, teststring, gender = None, ethnicity = None): + + if not gender is None: + if not gender in teststring: + return False + if not ethnicity is None: + if not ethnicity in teststring: + return False + + return True + + + def _createExportSettings(self, r): + self.exportPanel = mhapi.ui.createGroupBox("Export settings") + self.exportPanel.addWidget(mhapi.ui.createLabel("File name base")) + r.addUI("output", "fnbase", self.exportPanel.addWidget(mhapi.ui.createTextEdit("mass"))) + self.exportPanel.addWidget(mhapi.ui.createLabel("")) + + data = ["MHM","OBJ","MHX2","FBX","DAE"] + + self.exportPanel.addWidget(mhapi.ui.createLabel("File format")) + r.addUI("output", "fileformat", self.exportPanel.addWidget(mhapi.ui.createComboBox(data=data))) + + info = "\nFiles end up in the\n" + info +="usual directory, named\n" + info +="with the file name base\n" + info +="plus four digits plus\n" + info +="file extension." + + self.exportPanel.addWidget(mhapi.ui.createLabel(info)) + + return self.exportPanel + + def _createProducePanel(self, r): + self.producePanel = mhapi.ui.createGroupBox("Produce") + + self.producePanel.addWidget(mhapi.ui.createLabel("Number of characters")) + r.addUI("output", "numfiles", self.producePanel.addWidget(mhapi.ui.createTextEdit("5"))) + self.producePanel.addWidget(mhapi.ui.createLabel("")) + self.produceButton = self.producePanel.addWidget(mhapi.ui.createButton("Produce")) + + @self.produceButton.mhEvent + def onClicked(event): + self._onProduceClick() + + return self.producePanel + + def _createModelingSettings(self, r): + self.modelingPanel = mhapi.ui.createGroupBox("Modeling settings") + + defaultUnchecked = ["arms","hands","legs","feet"] + + mfi = ModifierInfo() + gn = mfi.getModifierGroupNames() + for n in gn: + sel = not n in defaultUnchecked + label = n + if n == "breast": + label = "breasts (if fem)" + r.addUI("modeling", n, self.modelingPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize " + label, selected=sel))) + + self.modelingPanel.addWidget(mhapi.ui.createLabel()) + r.addUI("modeling", "maxdev", self.modelingPanel.addWidget(mhapi.ui.createSlider(value=0.3, min=0.0, max=1.0, label="Max deviation from default"))) + + #self.modelingPanel.addWidget(mhapi.ui.createLabel()) + #r.addUI("modeling", "symmetry", self.modelingPanel.addWidget(mhapi.ui.createSlider(value=0.7, min=0.0, max=1.0, label="Symmetry"))) + + return self.modelingPanel + + def _createMacroSettings(self, r): + self.macroPanel = mhapi.ui.createGroupBox("Macro settings") + + r.addUI("macro", "randomizeAge", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize age", selected=True))) + r.addUI("macro", "ageMinimum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Minimum age", value=0.45))) + r.addUI("macro", "ageMaximum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Maximum age", value=0.95))) + + self.macroPanel.addWidget(mhapi.ui.createLabel()) + + r.addUI("macro", "randomizeWeight", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize weight", selected=True))) + r.addUI("macro", "weightMinimum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Minimum weight", value=0.1))) + r.addUI("macro", "weightMaximum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Maximum weight", value=0.9))) + + self.macroPanel.addWidget(mhapi.ui.createLabel()) + + r.addUI("macro", "randomizeHeight", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize height", selected=True))) + r.addUI("macro", "heightMinimum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Minimum height", value=0.2))) + r.addUI("macro", "heightMaximum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Maximum height", value=0.9))) + + self.macroPanel.addWidget(mhapi.ui.createLabel()) + + r.addUI("macro", "randomizeMuscle", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize muscle", selected=True))) + r.addUI("macro", "muscleMinimum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Minimum muscle", value=0.3))) + r.addUI("macro", "muscleMaximum", self.macroPanel.addWidget(mhapi.ui.createSlider(label="Maximum muscle", value=0.8))) + + self.macroPanel.addWidget(mhapi.ui.createLabel()) + + r.addUI("macro", "gender", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize gender", selected=True))) + r.addUI("macro", "genderabsolute", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Absolute gender", selected=True))) + + self.macroPanel.addWidget(mhapi.ui.createLabel()) + + r.addUI("macro", "ethnicity", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Randomize ethnicity", selected=True))) + r.addUI("macro", "ethnicityabsolute", self.macroPanel.addWidget(mhapi.ui.createCheckBox(label="Absolute ethnicity", selected=True))) + + return self.macroPanel + + def _onProduceClick(self): + #print("Produce") + + #self.randomizationSettings.dumpValues() + + self.initialState = HumanState() + + i = int(self.randomizationSettings.getValue("output","numfiles")) + base = self.randomizationSettings.getValue("output","fnbase") + + max = i + + prog = Progress() + + while i > 0: + prg = float(max - i + 1) / float(max) + prgStr = str( max - i + 1) + " / " + str(max) + prog(prg, desc="Randomizing " + prgStr) + self.nextState = HumanState(self.randomizationSettings) + self.nextState.applyState(False) + format = self.randomizationSettings.getValue("output","fileformat") + name = base + str(i).rjust(4,"0") + + prog(prg, desc="Exporting " + prgStr) + + if format == "MHM": + path = mhapi.locations.getUserHomePath("models") + if not os.path.exists(path): + os.makedirs(path) + name = name + ".mhm" + self.human.save(os.path.join(path,name)) + if format == "OBJ": + mhapi.exports.exportAsOBJ(name + ".obj") + if format == "DAE": + mhapi.exports.exportAsDAE(name + ".dae") + if format == "FBX": + mhapi.exports.exportAsFBX(name + ".fbx") + if format == "MHX2" or format == "MHX": + mhapi.exports.exportAsMHX2(name + ".mhx2") + + prog(prg, desc="Evaluating") + + i = i - 1 + self.initialState.applyState(True) + self.human.applyAllTargets() + + self.msg = QMessageBox() + self.msg.setIcon(QMessageBox.Information) + self.msg.setText("Done!") + self.msg.setWindowTitle("Produce") + self.msg.setStandardButtons(QMessageBox.Ok) + self.msg.show() diff --git a/makehuman/plugins/9_massproduce/modifiergroups.py b/makehuman/plugins/9_massproduce/modifiergroups.py new file mode 100644 index 000000000..13332dbcd --- /dev/null +++ b/makehuman/plugins/9_massproduce/modifiergroups.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +MACROGROUPS = dict() +MACROGROUPS["age"] = ["macrodetails/Age"] +MACROGROUPS["height"] = ["macrodetails-height/Height"] +MACROGROUPS["weight"] = ["macrodetails-universal/Weight"] +MACROGROUPS["muscle"] = ["macrodetails-universal/Muscle"] +MACROGROUPS["gender"] = ["macrodetails/Gender"] +MACROGROUPS["proportion"] = ["macrodetails-proportions/BodyProportions"] +MACROGROUPS["ethnicity"] = ["macrodetails/African", "macrodetails/Asian", "macrodetails/Caucasian"] + +from core import G +mhapi = G.app.mhapi + +class _ModifierInfo(): + + def __init__(self): + + self.human = mhapi.internals.getHuman() + self.nonMacroModifierGroupNames = [] + self.modifierInfo = dict() + + for modgroup in self.human.modifierGroups: + #print("MODIFIER GROUP: " + modgroup) + if not "macro" in modgroup and not "genitals" in modgroup and not "armslegs" in modgroup: + self.nonMacroModifierGroupNames.append(modgroup) + self.modifierInfo[modgroup] = [] + for mod in self.human.getModifiersByGroup(modgroup): + if not mod.name.startswith("r-"): + self.modifierInfo[modgroup].append(self._deduceModifierInfo(modgroup, mod)) + + self.nonMacroModifierGroupNames.append("arms") + self.modifierInfo["arms"] = [] + + self.nonMacroModifierGroupNames.append("hands") + self.modifierInfo["hands"] = [] + + self.nonMacroModifierGroupNames.append("legs") + self.modifierInfo["legs"] = [] + + self.nonMacroModifierGroupNames.append("feet") + self.modifierInfo["feet"] = [] + + for mod in self.human.getModifiersByGroup("armslegs"): + name = mod.name + if not name.startswith("r-"): + if "hand" in name: + self.modifierInfo["hands"].append(self._deduceModifierInfo("hands", mod)) + if "foot" in name or "feet" in name: + self.modifierInfo["feet"].append(self._deduceModifierInfo("feet", mod)) + if "arm" in name: + self.modifierInfo["arms"].append(self._deduceModifierInfo("arms", mod)) + if "leg" in name: + self.modifierInfo["legs"].append(self._deduceModifierInfo("legs", mod)) + + self.nonMacroModifierGroupNames.sort() + + def _deduceModifierInfo(self,groupName,modifier): + modi = dict() + modi["modifier"] = modifier + modi["name"] = modifier.name + modi["actualGroupName"] = modifier.groupName + modi["groupName"] = groupName + # This is the value set at the point when the user starts producing, + # rather than makehuman's standard default value + modi["defaultValue"] = modifier.getValue() + modi["twosided"] = modifier.getMin() < -0.05 + modi["leftright"] = modifier.name.startswith('l-') + return modi + + def getModifierGroupNames(self): + return list(self.nonMacroModifierGroupNames) + + def getModifierInfoForGroup(self, groupName): + return list(self.modifierInfo[groupName]) + +_mfinstance = None + +def ModifierInfo(): + global _mfinstance + if _mfinstance is None: + _mfinstance = _ModifierInfo() + return _mfinstance diff --git a/makehuman/plugins/9_massproduce/randomizationsettings.py b/makehuman/plugins/9_massproduce/randomizationsettings.py new file mode 100644 index 000000000..20392b07d --- /dev/null +++ b/makehuman/plugins/9_massproduce/randomizationsettings.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from PyQt5.QtWidgets import * +import sys +import qtgui + +class RandomizationSettings: + + def __init__(self): + + self._ui = dict() + + def addUI(self, category, name, widget, subName=None): + + if widget is None: + raise ValueError("Trying to add None widget") + + if not category in self._ui: + self._ui[category] = dict() + + if not subName is None: + if not name in self._ui[category]: + self._ui[category][name] = dict() + self._ui[category][name][subName] = widget + else: + self._ui[category][name] = widget + + return widget + + def getUI(self, category, name, subName=None): + + if not category in self._ui: + print("No such category: " + category) + self.dumpValues() + sys.exit() + + if not name in self._ui[category]: + print("No such name: " + category + "/" + name) + self.dumpValues() + sys.exit() + + if not subName is None: + if not subName in self._ui[category][name]: + print("No such subName: " + category + "/" + name + "/" + subName) + self.dumpValues() + sys.exit() + widget = self._ui[category][name][subName] + else: + widget = self._ui[category][name] + + if widget is None: + print("Got None widget for " + category + "/" + name + "/" + str(subName)) + return widget + + def getValue(self, category, name, subName=None): + + widget = self.getUI(category, name, subName) + + if isinstance(widget, QCheckBox) or isinstance(widget, qtgui.CheckBox): + return widget.selected + if isinstance(widget, QTextEdit) or isinstance(widget, qtgui.TextEdit): + return widget.getText() + if isinstance(widget, qtgui.Slider): + return widget.getValue() + if isinstance(widget, QComboBox): + return str(widget.getCurrentItem()) + if isinstance(widget, str): + return widget + + print("Unknown widget type") + print(type(widget)) + sys.exit(1) + + def getValueHash(self, category, name): + + if not category in self._ui: + print("No such category: " + category) + self.dumpValues() + sys.exit() + + if not name in self._ui[category]: + print("No such name: " + category + "/" + name) + self.dumpValues() + sys.exit() + + subCat = self._ui[category][name] + + if not isinstance(subCat, dict): + print(category + "/" + name + " is not a dict") + self.dumpValues() + sys.exit(1) + + values = dict() + for subName in subCat: + values[subName] = self.getValue(category, name, subName) + + return values + + def getNames(self, category): + return self._ui[category].keys() + + def setValue(self, category, name, value, subName=None): + pass + + def dumpValues(self): + + for category in self._ui: + print(category) + for name in self._ui[category]: + widget = self.getUI(category, name) + if isinstance(widget, dict): + print(" " + name) + for subName in widget: + subWidget = self.getUI(category, name, subName) + value = self.getValue(category, name, subName) + print(" " + subName + " (" + str(type(subWidget)) + ") = " + str(value)) + else: + value = self.getValue(category, name) + print(" " + name + " (" + str(type(widget)) + ") = " + str(value)) diff --git a/makehuman/plugins/9_massproduce/randomizeaction.py b/makehuman/plugins/9_massproduce/randomizeaction.py new file mode 100644 index 000000000..7a83146d1 --- /dev/null +++ b/makehuman/plugins/9_massproduce/randomizeaction.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import gui3d + +mhapi = gui3d.app.mhapi + +class RandomizeAction(gui3d.Action): + def __init__(self, human, before, after): + super(RandomizeAction, self).__init__("Randomize") + self.human = human + self.before = before + self.after = after + + def do(self): + self._assignModifierValues(self.after) + return True + + def undo(self): + self._assignModifierValues(self.before) + return True + + def _assignModifierValues(self, valuesDict): + _tmp = self.human.symmetryModeEnabled + self.human.symmetryModeEnabled = False + for mName, val in list(valuesDict.items()): + try: + self.human.getModifier(mName).setValue(val) + except: + pass + self.human.applyAllTargets() + self.human.symmetryModeEnabled = _tmp \ No newline at end of file
Name" + g + p + "
" + name + "XXX---