├── gozbruh ├── __init__.py ├── start_zbrush.py ├── DefaultZScript.txt ├── errs.py ├── cmd.py ├── zbrushgui.py ├── mayagui.py ├── utils.py ├── maya_tools.py └── zbrush_tools.py ├── .gitignore ├── setup.py ├── README.md └── scripts └── goz_config /gozbruh/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gozbruh/start_zbrush.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Not currently used 4 | import zbrushgui 5 | zbg = zbrushgui.ZBrushGUI() 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | <<<<<<< HEAD 3 | .swp 4 | .zsc 5 | ======= 6 | *.pyc 7 | *.swp 8 | >>>>>>> 43559e50075ee93cf7f5bfb0c2f0c87a58f14f02 9 | -------------------------------------------------------------------------------- /gozbruh/DefaultZScript.txt: -------------------------------------------------------------------------------- 1 | //Startup 2 | [If, [MemGetSize, startup_mem],, 3 | [MVarDef, startup_mem, 1, 0] 4 | 5 | //Execute Utils for setup 6 | [ShellExecute, "/usr/bin/python #GOZ_COMMAND_SCRIPT start_server"] 7 | ] 8 | [pd] 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='gozbruh', 6 | version='1.1.3', 7 | description='Cross-platform and cross-machine file sync for ZBrush', 8 | author='Luma Pictures', 9 | url='lumapictures.com', 10 | platforms='Unix,OSX', 11 | packages=['gozbruh'], 12 | package_dir={'gozbruh': 'gozbruh'}, 13 | package_data={'goghz': [os.path.join('gozbruh', '*.txt')]}, 14 | scripts=[os.path.join('scripts', 'goz_config')] 15 | ) 16 | -------------------------------------------------------------------------------- /gozbruh/errs.py: -------------------------------------------------------------------------------- 1 | """custom exceptions for go-zbruh""" 2 | 3 | class GozbruhError(Exception): 4 | pass 5 | 6 | 7 | class IpError(GozbruhError): 8 | """Exception raised for invalid IP addresses 9 | 10 | Attribitues 11 | ----------- 12 | host : str 13 | input host address 14 | msg : str 15 | gui message 16 | 17 | """ 18 | 19 | def __init__(self, host, msg): 20 | GozbruhError.__init__(self, msg) 21 | self.host = host 22 | self.msg = msg 23 | 24 | 25 | class ZBrushServerError(GozbruhError): 26 | """Exception raised for connection failure 27 | 28 | Attribitues 29 | ----------- 30 | msg : str 31 | gui message 32 | 33 | """ 34 | 35 | 36 | class PortError(GozbruhError): 37 | """Exception raised for invalid socket ports 38 | 39 | Attributes 40 | ---------- 41 | port : str 42 | input port 43 | msg : str 44 | gui msg 45 | 46 | """ 47 | 48 | def __init__(self, port, msg): 49 | GozbruhError.__init__(self, msg) 50 | self.port = port 51 | self.msg = msg 52 | self.message = msg 53 | 54 | 55 | class SelectionError(GozbruhError): 56 | """Exception raise for no file mesh selected 57 | 58 | Attributes 59 | ---------- 60 | msg : str 61 | gui msg 62 | 63 | """ 64 | -------------------------------------------------------------------------------- /gozbruh/cmd.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | this_module = os.path.abspath(sys.modules[__name__].__file__) 5 | parent_dir = os.path.dirname(os.path.dirname(this_module)) 6 | sys.path.append(parent_dir) 7 | 8 | 9 | if __name__ == "__main__": 10 | """Grabs args for when this module is run as a script 11 | """ 12 | # TODO: convert to argparse. not doing that yet, because it is not included 13 | # in python until 2.7... could do optparse... 14 | assert len(sys.argv) > 1, "You must pass a command" 15 | command = sys.argv[1] 16 | 17 | if command == 'send': 18 | import gozbruh.zbrush_tools 19 | gozbruh.zbrush_tools.ZBrushToMayaClient.send(sys.argv[2], sys.argv[3]) 20 | elif command == 'serve': 21 | import gozbruh.zbrush_tools 22 | gozbruh.zbrush_tools.start_zbrush_server() 23 | elif command == 'install': 24 | gozbruh.utils 25 | # FIXME: I don't think this is used anymore... 26 | # We are performing a part of the installation 27 | gozbruh.utils.install_goz() 28 | elif command == 'start_server': 29 | # Start the server on a new background thread (i.e. don't call 30 | # communicate) 31 | # This code is reached from a ShellExecute command in 32 | # DefaultZScript.txt 33 | # 34 | # NOTE: it seems like there should be a better way to do this, but 35 | # to improve it we first have to confirm/dispell a few things that 36 | # this code seems to assume: 37 | # - assumption 1: it is not possible to create a background 38 | # sub-process in zscript using ShellExecute, and we therefore need 39 | # to create a second shell sub-process from here. 40 | # - assumption 2: an executable script that is pip installed on osx 41 | # cannot import its corresponding python module given the env 42 | # provided when calling ShellExecute from ZBrush, and we 43 | # therefore need to make our modules double as executables 44 | import subprocess 45 | this_module = os.path.abspath(sys.modules[__name__].__file__) 46 | subprocess.Popen('python %s %s' % (this_module, 'serve'), shell=True) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-zbruh Command Port 2 | 3 | Sync meshes between Maya and ZBrush running on different workstations. 4 | 5 | Limitations: 6 | - If using two machines, they must have a shared network drive 7 | - ZBrush is currently only supported on OSX. We welcome a pull request from 8 | someone from the Windows world. 9 | 10 | 11 | ## Installation 12 | 13 | ### ZBrush 14 | 15 | 1. Install the python module on the ZBrush workstation: 16 | 17 | ``` 18 | git clone https://github.com/LumaPictures/gozbruh 19 | cd gozbruh 20 | python setup.py install 21 | ``` 22 | 23 | 2. Run the configuration script UI (If your system is setup properly, 24 | the previous step should have installed the script on your executable path, 25 | otherwise, the script will be in your python bin/scripts directory): 26 | 27 | ``` 28 | goz_config 29 | ``` 30 | 31 | Set up your configuration options and write the config files. 32 | For ZBrush machines, you may be presented with an option to choose which 33 | currently installed versions of ZBrush that you would like to install the 34 | files for. 35 | 36 | ### Maya 37 | 38 | 1. Install the python module on the Maya workstation: 39 | 40 | ``` 41 | git clone https://github.com/LumaPictures/gozbruh 42 | cd gozbruh 43 | python setup.py install 44 | ``` 45 | 46 | 2. Create a method for starting the Maya Command Port server either in 47 | startup, or as a button on a shelf doing something like the following: 48 | 49 | ```python 50 | import gozbruh.maya_tools as maya_tools 51 | maya_tools.start_maya_server() 52 | ``` 53 | 54 | 3. Create a shelf button for sending information to zbrush. 55 | 56 | You can use the GUI: 57 | 58 | ```python 59 | import gozbruh.mayagui as mayagui 60 | sendWin = mayagui.Win() 61 | ``` 62 | 63 | Or send the current selection with previously saved options: 64 | 65 | ```python 66 | import gozbruh.maya_tools as maya_tools 67 | maya_tools.send() 68 | ``` 69 | 70 | 71 | ## Other Options 72 | 73 | You have the option to add executable scripts which are run prior to starting up the tool. 74 | The are available for either application in the configuration directory: 75 | 76 | * The default configuration directory is ~/.zbrush/gozbruh/ 77 | * The default ZBrush pre-execution script is ~/.zbrush/gozbruh/ZBrushPreExec 78 | * The default Maya pre-execution script is ~/.zbrush/gozbruh/MayaPreExec 79 | 80 | ## Troubleshooting 81 | 82 | As long as the setup occured correctly and the configuration directory and DefaultZScript.txt is present for ZBrush and the configuration directory is present for the Maya machine everything should work properly. 83 | 84 | * If you see a .gozbruhLog file in your home directory, you are missing either the config directory, 85 | or the DefaultZScript directory. 86 | * If you see a message about incorrect paths in ZBrush, check configuration files 87 | * If you see a message about connectivity issues, or experience connectivity issues in either application, check your configuration files. 88 | -------------------------------------------------------------------------------- /gozbruh/zbrushgui.py: -------------------------------------------------------------------------------- 1 | """ZBrushGUI uses zbrushtools, starts ZBrushServer and ZBrushToMayaClient """ 2 | 3 | import Tkinter 4 | import tkMessageBox 5 | from . import zbrush_tools 6 | from . import utils 7 | 8 | 9 | class ZBrushGUI(object): 10 | """GUI for zbrush_tools 11 | 12 | build a UI using Tkinter, gets network info from utils.get_net_info 13 | starts ZBrushServer and ZBrushToMayaClient from zbrush_tools 14 | 15 | Also installs a zscript GUI in ZBrush using zbrush_tools.activate_zscript_ui 16 | 17 | Attributes 18 | ---------- 19 | serv : `ZBrushServer` 20 | ZBrushServer instance 21 | client : `ZBrushToMayaClient` 22 | ZBrushToMayaClient instance 23 | maya_status_ui : `Tkinter.Label` 24 | zbrush_status_ui : `Tkinter.Label` 25 | maya_host_ui : `Tkinter.Entry` 26 | maya_port_ui : `Tkinter.Entry` 27 | zbrush_port_ui : `Tkinter.Entry` 28 | """ 29 | 30 | def __init__(self): 31 | zhost, zport = utils.get_net_info(utils.ZBRUSH_ENV) 32 | mhost, mport = utils.get_net_info(utils.MAYA_ENV) 33 | 34 | self.serv = zbrush_tools.ZBrushServer(zhost, zport) 35 | self.client = zbrush_tools.ZBrushToMayaClient(mhost, mport) 36 | 37 | self.maya_status_ui = None 38 | self.maya_host_ui = None 39 | self.maya_port_ui = None 40 | 41 | self.zbrush_port_ui = None 42 | self.zbrush_status_ui = None 43 | self.win = None 44 | 45 | self.build() 46 | self.serv_start() 47 | self.test_client() 48 | self.activate_zscript_ui() 49 | 50 | def end_server(): 51 | self.serv_stop() 52 | self.win.destroy() 53 | 54 | self.win.protocol('WM_DELETE_WINDOW', end_server) 55 | self.win.mainloop() 56 | 57 | def serv_start(self): 58 | """Starts sever 59 | 60 | gets network info from UI (port) 61 | 62 | sets status line 63 | """ 64 | self.serv.port = self.zbrush_port_ui.get() 65 | 66 | with zbrush_tools.utils.err_handler(self.error_gui): 67 | self.serv.start() 68 | 69 | if self.serv.status: 70 | status_line = 'ZBrush Server Status: %s:%s' % ( 71 | self.serv.host, self.serv.port) 72 | 73 | self.zbrush_status_ui.config(text=status_line, background='green') 74 | else: 75 | self.zbrush_status_ui.config( 76 | text='ZBrush Server Status: down', 77 | background='red') 78 | 79 | def serv_stop(self): 80 | """Stops server 81 | 82 | sets status line 83 | 84 | """ 85 | if self.serv.server_thread.isAlive(): 86 | self.serv.stop() 87 | self.zbrush_status_ui.config( 88 | text='ZBrush Server Status: down', 89 | background='red') 90 | 91 | def activate_zscript_ui(self): 92 | """install UI in ZBrush """ 93 | 94 | zbrush_tools.activate_zbrush() 95 | zbrush_tools.activate_zscript_ui() 96 | 97 | def test_client(self): 98 | """Tests conn to MayaSever 99 | """ 100 | 101 | self.client.host = self.maya_host_ui.get() 102 | self.client.port = self.maya_port_ui.get() 103 | 104 | self.maya_status_ui.config( 105 | text='ZBrushToMayaClient Status: conn refused', 106 | background='red') 107 | 108 | with zbrush_tools.utils.err_handler(self.error_gui): 109 | ret = self.client.test_client() 110 | 111 | if ret: 112 | print 'connected to maya' 113 | self.maya_status_ui.config( 114 | text='ZBrushToMayaClient Status: connected', 115 | background='green') 116 | 117 | def build(self): 118 | """Creates tkinter UI 119 | """ 120 | self.win = Tkinter.Tk() 121 | self.win.title('gozbruh GUI') 122 | Tkinter.Label( 123 | self.win, 124 | text='gozbruh - Basic Setup:').pack(pady=5, padx=25) 125 | 126 | Tkinter.Label( 127 | self.win, 128 | text='Set MNET/ZNET/ZDOCS envs').pack(pady=0, padx=25) 129 | Tkinter.Label( 130 | self.win, 131 | text='like: ZNET=127.0.0.1:6668').pack(pady=0, padx=25) 132 | Tkinter.Label( 133 | self.win, 134 | text='set ZDOCS to your network path').pack(pady=0, padx=25) 135 | 136 | zb_cfg = Tkinter.LabelFrame(self.win, text="ZBrush Server") 137 | zb_cfg.pack(pady=15, fill="both", expand="yes") 138 | 139 | Tkinter.Label(zb_cfg, text='ZBrush Port:').pack(pady=5, padx=5) 140 | self.zbrush_port_ui = Tkinter.Entry(zb_cfg, width=15) 141 | self.zbrush_port_ui.pack() 142 | self.zbrush_port_ui.insert(0, self.serv.port) 143 | 144 | Tkinter.Button( 145 | zb_cfg, 146 | text='Start', 147 | command=self.serv_start).pack() 148 | Tkinter.Button( 149 | zb_cfg, 150 | text='Stop', 151 | command=self.serv_stop).pack() 152 | 153 | self.zbrush_status_ui = Tkinter.Label( 154 | zb_cfg, 155 | text='ZBrush Server Status: down', 156 | background='red') 157 | self.zbrush_status_ui.pack(pady=5, padx=5) 158 | 159 | maya_cfg = Tkinter.LabelFrame(self.win, text="Maya Client") 160 | maya_cfg.pack(pady=15, fill="both", expand="yes") 161 | 162 | Tkinter.Label(maya_cfg, text='Maya Host:').pack(pady=5, padx=5) 163 | self.maya_host_ui = Tkinter.Entry(maya_cfg, width=15) 164 | self.maya_host_ui.insert(0, self.client.host) 165 | self.maya_host_ui.pack() 166 | 167 | Tkinter.Label(maya_cfg, text='Maya Port:').pack(pady=5, padx=5) 168 | self.maya_port_ui = Tkinter.Entry(maya_cfg, width=15) 169 | self.maya_port_ui.insert(0, self.client.port) 170 | self.maya_port_ui.pack() 171 | 172 | Tkinter.Button( 173 | maya_cfg, 174 | text='Make ZBrush UI', 175 | command=self.activate_zscript_ui).pack() 176 | Tkinter.Button( 177 | maya_cfg, 178 | text='Test Connection', 179 | command=self.test_client).pack() 180 | self.maya_status_ui = Tkinter.Label( 181 | maya_cfg, 182 | text='Maya Client Status: conn refused', 183 | background='red') 184 | self.maya_status_ui.pack(pady=5, padx=5) 185 | 186 | @staticmethod 187 | def error_gui(message): 188 | """Simple tkinter gui for displaying errors 189 | """ 190 | tkMessageBox.showwarning('gozbruh Error:', message) 191 | -------------------------------------------------------------------------------- /scripts/goz_config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """GUI for setting up and creating the gozbruh configuration directory and files """ 3 | 4 | import os 5 | import sys 6 | 7 | from gozbruh import zbrush_tools as zbrush_tools 8 | import Tkinter 9 | import tkMessageBox 10 | import gozbruh.utils as utils 11 | import time 12 | 13 | class FileSelector(Tkinter.Toplevel): 14 | """Tkinter.Toplevel class that allows the user to select ZBrush 15 | installation directories from a list of files. 16 | """ 17 | 18 | def __init__(self, file_list): 19 | Tkinter.Toplevel.__init__(self) 20 | self.minsize(400, 300) 21 | 22 | self.file_list = file_list 23 | self.build() 24 | 25 | def get_selection(self): 26 | """Gets a list of str for the currently selected items in the list_box 27 | """ 28 | sel_tup = self.list_box.curselection() 29 | sel_list = set() 30 | for index in sel_tup: 31 | sel_list.add(self.list_box.get(index, index)) 32 | return sel_list 33 | 34 | def build(self): 35 | """Builds the window and listbox inside 36 | """ 37 | 38 | Tkinter.Label(self, 39 | text='Multiple ZBrush Install Directories Found...\n' 40 | 'Please Choose the ones in which you\'d like to' 41 | ' install gozbruh').pack() 42 | 43 | self.list_box = Tkinter.Listbox(self, 44 | selectmode=Tkinter.MULTIPLE, 45 | height='15', 46 | width='80') 47 | for item in self.file_list: 48 | self.list_box.insert(Tkinter.END, item) 49 | self.list_box.pack() 50 | 51 | btn_frame = Tkinter.Frame(self) 52 | btn_frame.pack() 53 | 54 | self.accept_btn = Tkinter.Button(btn_frame) 55 | self.accept_btn['text'] = 'Ok' 56 | self.accept_btn['command'] = lambda: self.event_generate('<>') 57 | self.accept_btn.pack(side='left') 58 | 59 | self.cancel_btn = Tkinter.Button(btn_frame) 60 | self.cancel_btn['text'] = 'Cancel' 61 | self.cancel_btn['command'] = self.destroy 62 | self.cancel_btn.pack(side='left') 63 | 64 | class ConfigUI(object): 65 | """Class to handle the end-user configuration of the gozbruh Cross-Platform 66 | application. 67 | """ 68 | 69 | def __init__(self): 70 | self.maya_host, self.maya_port = utils.get_net_info(utils.MAYA_ENV) 71 | self.zbrush_host, self.zbrush_port = utils.get_net_info(utils.ZBRUSH_ENV) 72 | self.shared_dir = utils.get_shared_dir() 73 | 74 | self.maya_host_ui = None 75 | self.maya_port_ui = None 76 | self.maya_status_ui = None 77 | 78 | self.shared_dir_ui = None 79 | 80 | self.zbrush_host_ui = None 81 | self.zbrush_port_ui = None 82 | self.zbrush_status_ui = None 83 | self.win = None 84 | 85 | self.fs = None 86 | 87 | self.build() 88 | self.check_servers() 89 | 90 | self.win.mainloop() 91 | 92 | def update_data(self): 93 | """Gets all of the new user-entered data if anything has changed. 94 | """ 95 | self.maya_host = self.maya_host_ui.get() 96 | self.maya_port = self.maya_port_ui.get() 97 | 98 | self.zbrush_host = self.zbrush_host_ui.get() 99 | self.zbrush_port = self.zbrush_port_ui.get() 100 | 101 | self.shared_dir = self.shared_dir_ui.get() 102 | 103 | def check_servers(self): 104 | """Update the server status and check to see if they are up and running 105 | """ 106 | 107 | # Get new host information from the boxes 108 | self.update_data() 109 | 110 | # Check server status based on what servers we're looking at 111 | if utils.validate_connection(self.zbrush_host, self.zbrush_port): 112 | self.zbrush_status_ui.config( 113 | text=('ZBrush Serv Status: connected %s:%s ' % 114 | (self.zbrush_host, self.zbrush_port)), 115 | background='green') 116 | else: 117 | self.zbrush_status_ui.config( 118 | text='ZBrush Serv Status: down', 119 | background='red') 120 | 121 | if utils.validate_connection(self.maya_host, self.maya_port): 122 | self.maya_status_ui.config( 123 | text=('Maya Serv Status: connected %s:%s ' % 124 | (self.maya_host, self.maya_port)), 125 | background='green') 126 | else: 127 | self.maya_status_ui.config( 128 | text='Maya Serv Status: down', 129 | background='red') 130 | 131 | def serv_stop(self): 132 | """Stops ZBrushServer 133 | """ 134 | utils.force_zbrush_server_close(host=self.zbrush_host, 135 | port=self.zbrush_port) 136 | while utils.validate_connection(self.zbrush_host, self.zbrush_port): 137 | time.sleep(.1) 138 | 139 | self.check_servers() 140 | 141 | def set_env_vars(self): 142 | """Set all of the appropriate env vars in prep for writing config files 143 | """ 144 | # For each server/host entry, write the appropriate config file 145 | write_data = {utils.MAYA_ENV: '%s:%s' % (self.maya_host, self.maya_port), 146 | utils.ZBRUSH_ENV: '%s:%s' % (self.zbrush_host, self.zbrush_port), 147 | utils.SHARED_DIR_ENV: self.shared_dir} 148 | for env_var_key in write_data: 149 | os.environ[env_var_key] = write_data[env_var_key] 150 | 151 | def write_config_setup(self): 152 | """Takes the new user input and sets up everything necessary for the 153 | writing process 154 | """ 155 | # Update data for all of the new field entries 156 | self.update_data() 157 | 158 | # Set all of the environment variables so that they are written 159 | self.set_env_vars() 160 | 161 | # Write the CONFIG_PATH dirs/files if that already hasn't been done. 162 | # on the current machine. 163 | utils.create_config_path() 164 | 165 | if sys.platform == 'darwin': 166 | # Get locations that ZBrush is installed for possibility of multiple 167 | # writings of the DefaultZScript.txt 168 | zbrush_dirs = utils.get_zbrush_app_dirs() 169 | 170 | if zbrush_dirs is not None and len(zbrush_dirs) > 1: 171 | # If there are multiple locations, provide the user with a chance to 172 | # select which zbrush applications the DefaultZScript.txt should 173 | # be installed for 174 | self.fs = FileSelector(zbrush_dirs) 175 | self.fs.bind('<>', self.write_files) 176 | return 177 | else: 178 | self.set_env_vars() 179 | utils.install_goz(zbrush_dirs) 180 | tkMessageBox.showinfo('gozbruh Success:', 'Write Complete!') 181 | return 182 | tkMessageBox.showinfo('gozbruh Success:', 'Write Complete!') 183 | 184 | def write_files(self, offending_event): 185 | """Writes the DefaultZScirpt and other necessary files for each 186 | ZBrush application install directory in the set 187 | """ 188 | 189 | app_dirs = self.fs.get_selection() 190 | 191 | # For each directory selected, create the DefaultZScript 192 | for dir in app_dirs: 193 | self.set_env_vars() 194 | utils.install_goz(dir) 195 | tkMessageBox.showinfo('gozbruh Success:', 'Write Complete!') 196 | 197 | def build(self): 198 | """Creates Tkinter UI 199 | """ 200 | self.win = Tkinter.Tk() 201 | self.win.title('gozbruh Config GUI') 202 | self.win.minsize(475, 500) 203 | 204 | gen_cfg = Tkinter.LabelFrame(self.win, text="General Config") 205 | gen_cfg.pack(pady=15, fill="both", expand="yes") 206 | 207 | Tkinter.Label(gen_cfg, text='Shared Dir:').pack(pady=5, padx=5) 208 | self.shared_dir_ui = Tkinter.Entry(gen_cfg, width=30) 209 | self.shared_dir_ui.pack() 210 | self.shared_dir_ui.insert(0, str(self.shared_dir)) 211 | Tkinter.Label(gen_cfg, text='').pack(pady=5, padx=5) 212 | 213 | Tkinter.Button( 214 | self.win, 215 | text='Check Servers', 216 | command=self.check_servers).pack() 217 | 218 | zb_cfg = Tkinter.LabelFrame(self.win, text="ZBrush") 219 | zb_cfg.pack(pady=15, fill="both", expand="yes") 220 | 221 | Tkinter.Label(zb_cfg, text='ZBrush Host:').pack(pady=5, padx=5) 222 | self.zbrush_host_ui = Tkinter.Entry(zb_cfg, width=30) 223 | self.zbrush_host_ui.pack() 224 | self.zbrush_host_ui.insert(0, self.zbrush_host) 225 | 226 | Tkinter.Label(zb_cfg, text='ZBrush Port:').pack(pady=5, padx=5) 227 | self.zbrush_port_ui = Tkinter.Entry(zb_cfg, width=30) 228 | self.zbrush_port_ui.pack() 229 | self.zbrush_port_ui.insert(0, self.zbrush_port) 230 | 231 | Tkinter.Button( 232 | zb_cfg, 233 | text='Stop', 234 | command=self.serv_stop).pack() 235 | 236 | self.zbrush_status_ui = Tkinter.Label( 237 | zb_cfg, 238 | text='ZBrush Server Status: down', 239 | background='red') 240 | self.zbrush_status_ui.pack(pady=5, padx=5) 241 | 242 | maya_cfg = Tkinter.LabelFrame(self.win, text="Maya") 243 | maya_cfg.pack(pady=15, fill="both", expand="yes") 244 | 245 | Tkinter.Label(maya_cfg, text='Maya Host:').pack(pady=5, padx=5) 246 | self.maya_host_ui = Tkinter.Entry(maya_cfg, width=30) 247 | self.maya_host_ui.insert(0, self.maya_host) 248 | self.maya_host_ui.pack() 249 | 250 | Tkinter.Label(maya_cfg, text='Maya Port:').pack(pady=5, padx=5) 251 | self.maya_port_ui = Tkinter.Entry(maya_cfg, width=30) 252 | self.maya_port_ui.insert(0, self.maya_port) 253 | self.maya_port_ui.pack() 254 | 255 | self.maya_status_ui = Tkinter.Label( 256 | maya_cfg, 257 | text='Maya Client Status: conn refused', 258 | background='red') 259 | self.maya_status_ui.pack(pady=5, padx=5) 260 | 261 | Tkinter.Label( 262 | self.win, 263 | text=('Config will write to: %s' % 264 | utils.CONFIG_PATH)).pack(pady=5, padx=5) 265 | Tkinter.Button(self.win, 266 | text='Write Config', 267 | command=self.write_config_setup).pack() 268 | 269 | @staticmethod 270 | def error_gui(message): 271 | """Simple tkinter gui for displaying errors 272 | """ 273 | tkMessageBox.showwarning('gozbruh Error:', message) 274 | 275 | if __name__ == '__main__': 276 | ConfigUI() 277 | -------------------------------------------------------------------------------- /gozbruh/mayagui.py: -------------------------------------------------------------------------------- 1 | """Class to create a gui within maya uses gozbruh.maya_tools 2 | 3 | (Not currently used in the main functionality of the program except for send()) 4 | """ 5 | import os 6 | import sys 7 | 8 | import pymel.core as pm 9 | from . import maya_tools 10 | from . import utils 11 | 12 | 13 | class Win(object): 14 | """GUI for maya_tools 15 | 16 | Attributes 17 | ---------- 18 | serv : MayaServer 19 | MayaServer instance 20 | client : MayaToZBrushClient 21 | MayaToZBrushClient instance 22 | user_* : str 23 | user defined network info 24 | maya_status_ui : bool 25 | connection status 26 | zbrush_status_gui : pymel label 27 | connection status Label 28 | """ 29 | 30 | def __init__(self): 31 | """ new server/client, make gui, start server """ 32 | 33 | self.serv = maya_tools.MayaServer() 34 | self.client = maya_tools.MayaToZBrushClient() 35 | self.gui_window = None 36 | self.send_btn = None 37 | self.user_zbrush_host = None 38 | self.user_zbrush_port = None 39 | self.listen_btn = None 40 | self.user_maya_host = None 41 | self.user_maya_port = None 42 | self.user_shared_dir = None 43 | self.maya_status_ui = None 44 | self.conn_btn = None 45 | self.zbrush_status_gui = None 46 | 47 | # make the gui 48 | self.build() 49 | self.buttons() 50 | # start MayaServer 51 | self.listen() 52 | # check MayaToZBrushClient connection to ZBrushServer 53 | self.check_connect() 54 | 55 | self.client.check_socket() 56 | self.check_status_ui() 57 | 58 | def update_network(self): 59 | """Sends host/port back to client/server instances 60 | """ 61 | 62 | self.client.host = self.user_zbrush_host.getText() 63 | self.client.port = self.user_zbrush_port.getText() 64 | 65 | self.serv.host = self.user_maya_host.getText() 66 | self.serv.port = self.user_maya_port.getText() 67 | 68 | self.shared_dir = self.user_shared_dir.getText() 69 | 70 | def check_connect(self, *args): 71 | """Connects to ZBrushServer using MayaToZBrushClient instance 72 | """ 73 | self.update_network() 74 | with maya_tools.utils.err_handler(self.error_gui): 75 | self.client.connect() 76 | self.check_status_ui() 77 | 78 | def check_status_ui(self): 79 | """Updates statuslines, connected/disconnected for zbrush 80 | """ 81 | # check if client is connected, set gui accordingly 82 | if self.client.status: 83 | self.zbrush_status_ui.setBackgroundColor((0.0, 1.0, 0.5)) 84 | self.zbrush_status_ui.setLabel( 85 | 'Status: connected (' + 86 | self.client.host + ':' + 87 | str(self.client.port) + ')') 88 | else: 89 | self.zbrush_status_ui.setBackgroundColor((1, 0, 0)) 90 | self.zbrush_status_ui.setLabel('Status: not connected') 91 | 92 | def send(self, *args): 93 | """Send to zbrush using client instance 94 | 95 | Assists in some handling of gozbruhBrushID name mistmatches, 96 | this is done here to easliy create GUI boxes for create/relink 97 | 98 | Client.get_gozid_mistmatches returns a list of gozbruhBrushID mistmatches 99 | that need to resolved before sending the object to ZBrushServer 100 | 101 | """ 102 | 103 | self.client.check_socket() 104 | try: 105 | self.check_status_ui() 106 | except: 107 | pass 108 | 109 | maya_tools.send(client=self.client) 110 | 111 | def listen(self, *args): 112 | """Sends back host/port to MayaServer, starts listening 113 | """ 114 | if utils.validate_connection(self.serv.host, self.serv.port): 115 | self.serv.stop() 116 | self.update_network() 117 | self.serv.status = False 118 | 119 | with maya_tools.utils.err_handler(self.error_gui): 120 | self.serv.start() 121 | 122 | # check if server is up, set gui accordingly 123 | if self.serv.status: 124 | self.maya_status_ui.setBackgroundColor((0.0, 1.0, 0.5)) 125 | self.maya_status_ui.setLabel( 126 | 'Status: listening (' + 127 | self.serv.host + ':' + 128 | str(self.serv.port) + ')') 129 | else: 130 | self.maya_status_ui.setBackgroundColor((1, 0, 0)) 131 | self.maya_status_ui.setLabel('Status: not listening') 132 | 133 | def set_env_vars(self): 134 | """Sets the environment variables in preparation for writing config 135 | files 136 | """ 137 | # For each server/host entry, write the appropriate config file 138 | write_data = {utils.MAYA_ENV: '%s:%s' % (self.serv.host, 139 | self.serv.port), 140 | utils.ZBRUSH_ENV: '%s:%s' % (self.client.host, 141 | self.client.port), 142 | utils.SHARED_DIR_ENV: self.shared_dir} 143 | for env_var_key in write_data: 144 | os.environ[env_var_key] = write_data[env_var_key] 145 | 146 | def write_config(self, *args): 147 | """Takes the new user input and sets up everything necessary for the 148 | writing process 149 | """ 150 | # Update data for all of the new field entries 151 | self.update_network() 152 | 153 | # Set env vars 154 | self.set_env_vars() 155 | 156 | # Write the CONFIG_PATH dirs/files on the current machine. 157 | utils.install_goz() 158 | 159 | choice = pm.confirmDialog(title="ZBrush Success", 160 | message='Config Write Complete!', 161 | button=['Ok']) 162 | 163 | def default_config(self, *args): 164 | """If retaining of settings is not desired, the configuration files 165 | must be removed. 166 | """ 167 | default_host, default_port = utils.DEFAULT_NET[utils.ZBRUSH_ENV].split(':') 168 | self.user_zbrush_host.setText(default_host) 169 | self.user_zbrush_port.setText(default_port) 170 | 171 | default_host, default_port = utils.DEFAULT_NET[utils.MAYA_ENV].split(':') 172 | self.user_maya_host.setText(default_host) 173 | self.user_maya_port.setText(default_port) 174 | 175 | default_shared = '' 176 | if sys.platform == 'darwin': 177 | default_shared = utils.SHARED_DIR_DEFAULT_OSX 178 | elif sys.platform == 'win32': 179 | default_shared = utils.SHARED_DIR_DEFAULT_WIN 180 | else: 181 | default_shared = utils.SHARED_DIR_DEFAULT_LINUX 182 | 183 | self.user_shared_dir.setText(default_shared) 184 | 185 | def build(self): 186 | """Constructs gui 187 | """ 188 | if pm.window('goz', exists=True): 189 | pm.deleteUI('goz', window=True) 190 | 191 | self.gui_window = pm.window('goz', title="gozbruh", rtf=True, 192 | width=1000, height=700) 193 | 194 | pm.setUITemplate('attributeEditorTemplate', pushTemplate=True) 195 | 196 | main_layout = pm.frameLayout(label='gozbruh Options', cll=False) 197 | pm.setParent(main_layout) 198 | #====================================================================== 199 | # GENERAL 200 | #====================================================================== 201 | #########SHARED 202 | pm.setParent(main_layout) 203 | general_framelayout = pm.frameLayout(label='General Options', 204 | cll=False) 205 | pm.setParent(general_framelayout) 206 | 207 | general_rcl = pm.rowColumnLayout(nc=2) 208 | pm.setParent(general_rcl) 209 | pm.text(label='Shared Dir\t') 210 | self.user_shared_dir = pm.textField(text=utils.get_shared_dir(), 211 | width=200) 212 | 213 | #====================================================================== 214 | # SERVER 215 | #====================================================================== 216 | pm.setParent(main_layout) 217 | #########ZBRUSH 218 | zbrush_layout = pm.frameLayout(label='ZBrush', cll=False) 219 | pm.setParent(zbrush_layout) 220 | zbrush_rcl = pm.rowColumnLayout(nc=2) 221 | zbrush_host, zbrush_port = utils.get_net_info(utils.ZBRUSH_ENV) 222 | pm.text(label='ZBrush Host\t') 223 | self.user_zbrush_host = pm.textField(text=zbrush_host, width=200) 224 | pm.text(label='ZBrush Port\t') 225 | self.user_zbrush_port = pm.textField(text=zbrush_port, width=200) 226 | 227 | pm.setParent(zbrush_layout) 228 | self.send_btn = pm.button(label="Send Selection to ZBrush") 229 | self.conn_btn = pm.button(label="Check Connection to ZBrush") 230 | self.zbrush_status_ui = pm.text(label='Status: not connected', 231 | height=30, 232 | enableBackground=True, 233 | backgroundColor=(1.0, 0.0, 0.0)) 234 | 235 | #########MAYA 236 | pm.setParent(main_layout) 237 | pm.text(' ') 238 | maya_layout = pm.frameLayout(label='Maya', cll=False) 239 | pm.setParent(maya_layout) 240 | maya_rcl = pm.rowColumnLayout(nc=2) 241 | maya_host, maya_port = utils.get_net_info(utils.MAYA_ENV) 242 | pm.text(label='Maya Host\t') 243 | self.user_maya_host = pm.textField(text=maya_host, width=200) 244 | pm.text(label='Maya Port\t') 245 | self.user_maya_port = pm.textField(text=maya_port, width=200) 246 | 247 | pm.setParent(maya_layout) 248 | self.listen_btn = pm.button(label="Listen for Meshes from ZBrush") 249 | self.maya_status_ui = pm.text(label='Status: not listening', 250 | height=30, 251 | enableBackground=True, 252 | backgroundColor=(1.0, 0.0, 0.0)) 253 | 254 | #====================================================================== 255 | # MORE OPTIONS 256 | #====================================================================== 257 | pm.setParent(main_layout) 258 | pm.text(' ') 259 | more_framelayout = pm.frameLayout(label='Maya-Specific Options', 260 | cll=False) 261 | pm.setParent(more_framelayout) 262 | more_rcl = pm.rowColumnLayout(nc=3) 263 | pm.text(label='Import Visual Smooth', width=200) 264 | self.smooth_radio_col = pm.radioCollection() 265 | self.smooth_radio_off = pm.radioButton( 266 | label='Off', 267 | onc=lambda x: pm.optionVar(iv=('gozbruh_smooth', 0))) 268 | self.smooth_radio_on = pm.radioButton( 269 | label='On', 270 | onc=lambda x: pm.optionVar(iv=('gozbruh_smooth', 1))) 271 | import_smooth = 0 272 | if pm.optionVar(ex='gozbruh_smooth'): 273 | import_smooth = pm.optionVar(q='gozbruh_smooth') 274 | pm.radioCollection( 275 | self.smooth_radio_col, e=True, 276 | sl=self.smooth_radio_on if import_smooth else self.smooth_radio_off) 277 | 278 | pm.text(label='Import Delete Old Mesh', width=200) 279 | self.delete_radio_col = pm.radioCollection() 280 | self.delete_radio_off = pm.radioButton( 281 | label='Off', 282 | onc=lambda x: pm.optionVar(iv=('gozbruh_delete', 0))) 283 | self.delete_radio_on = pm.radioButton( 284 | label='On', 285 | onc=lambda x: pm.optionVar(iv=('gozbruh_delete', 1))) 286 | import_delete = 0 287 | if pm.optionVar(ex='gozbruh_delete'): 288 | import_delete = pm.optionVar(q='gozbruh_delete') 289 | pm.radioCollection( 290 | self.delete_radio_col, e=True, 291 | sl=self.delete_radio_on if import_delete else self.delete_radio_off) 292 | 293 | pm.setParent(main_layout) 294 | self.retain_btn = pm.button(label="Save Settings", height=50) 295 | pm.text('\t') 296 | self.remove_btn = pm.button(label="Default Settings") 297 | 298 | self.gui_window.show() 299 | 300 | def buttons(self): 301 | """Attaches methods to callbacks 302 | """ 303 | self.send_btn.setCommand(self.send) 304 | self.conn_btn.setCommand(self.check_connect) 305 | self.listen_btn.setCommand(self.listen) 306 | self.retain_btn.setCommand(self.write_config) 307 | self.remove_btn.setCommand(self.default_config) 308 | 309 | @staticmethod 310 | def error_gui(message): 311 | """Simple gui for displaying errors 312 | """ 313 | pm.confirmDialog( 314 | title=str('gozbruh Error:'), 315 | message='\n' + str(message), 316 | button=['Ok']) 317 | 318 | @staticmethod 319 | def spacer(num): 320 | """Creates a spacer 321 | """ 322 | for _ in range(0, num): 323 | pm.separator(style='none') 324 | -------------------------------------------------------------------------------- /gozbruh/utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | Utilities for all gozbruh functionality 5 | 6 | Constants 7 | --------- 8 | CURRDIR : str 9 | Current file path directory 10 | SHARED_DIR_ENV : str 11 | String representing the shared_dir in other places 12 | SHARED_DIR_DEFAULT_* : str 13 | String representing the sahred_dir defaults for different platforms 14 | 15 | MAYA_ENV : str 16 | String representing the Maya environment 17 | ZBRUSH_ENV : str 18 | String representing the ZBrush environment 19 | GOZ_HELP : str 20 | String representing the gozbruh help 21 | GOZ_LOG_PATH_FILE 22 | String representing the location of the gozbruh Log if it is needed 23 | DEFAULT_NET : dict 24 | Dict containing the default values to the MAYA/ZBRUSH env keys above 25 | ENV_TO_CONFIG_FILE : dict 26 | Dict containing the names for the config files pertaining to the 27 | configurable keys above 28 | """ 29 | 30 | import sys 31 | import os 32 | import socket 33 | import time 34 | from contextlib import contextmanager 35 | 36 | from . import errs 37 | 38 | # FIXME: will this affect all of python or just this file? 39 | sys.dont_write_bytecode = True 40 | 41 | # Default Paths 42 | # ------------- 43 | 44 | CONFIG_PATH = os.path.abspath(os.path.join(os.environ['HOME'], '.zbrush', 'gozbruh')) 45 | 46 | # currently only OSX is supported due to apple script usage 47 | SHARED_DIR_DEFAULT_OSX = '/Users/Shared/Pixologic/gozbruhProjects' 48 | # win32 api could be used on windows 49 | SHARED_DIR_DEFAULT_WIN = 'C:\\Users\\Public\\Pixologic\\gozbruhProjects' 50 | 51 | SHARED_DIR_DEFAULT_LINUX = os.path.join(CONFIG_PATH, 'temp') 52 | 53 | GOZ_LOG_PATH_FILE = os.path.join(os.environ['HOME'], '.gozbruhLog') 54 | 55 | # Environment Variables 56 | # ---------------------- 57 | 58 | SHARED_DIR_ENV = 'SHARED_ZDOCS' 59 | 60 | # maya network info env 61 | MAYA_ENV = 'MAYA_HOST' 62 | # zbrush network info env 63 | ZBRUSH_ENV = 'ZBRUSH_HOST' 64 | # default network info 65 | DEFAULT_NET = {MAYA_ENV: ':6667', ZBRUSH_ENV: ':6668'} 66 | 67 | # Configuration Files 68 | # ------------------- 69 | # we use multiple files instead of a single configuration file (json, ini, etc) 70 | # because we may need to read these from ZBrush, which is very crippled in 71 | # terms of scripting. 72 | ENV_TO_CONFIG_FILE = { 73 | MAYA_ENV: 'MayaHost', 74 | ZBRUSH_ENV: 'ZBrushHost', 75 | SHARED_DIR_ENV: 'ShareDir' 76 | } 77 | GOZ_HELP = '.gozbruhConfigHelp' 78 | ZBRUSH_PRE_EXEC = 'ZBrushPreExec' 79 | MAYA_PRE_EXEC = 'MayaPreExec' 80 | 81 | @contextmanager 82 | def err_handler(gui): 83 | """Handles general gozbruh errors, raises a gui/logger on err 84 | """ 85 | 86 | try: 87 | yield 88 | except (errs.PortError, 89 | errs.IpError, 90 | errs.SelectionError, 91 | errs.ZBrushServerError) as err: 92 | print err.msg 93 | gui(err.msg) 94 | except Exception as err: 95 | print err 96 | gui(err) 97 | finally: 98 | pass 99 | 100 | def validate_port(port): 101 | """Checks port is valid,or raises an error 102 | """ 103 | 104 | try: 105 | port = int(port) 106 | except ValueError: 107 | raise errs.PortError(port, 'Please specify a valid port: %s' % (port)) 108 | 109 | def validate_host(host): 110 | """Validates IP/host, or raises and error 111 | """ 112 | 113 | try: 114 | host = socket.gethostbyname(host) 115 | except socket.error: 116 | raise errs.IpError(host, 'Please specify a valid host: %s' % (host)) 117 | 118 | def validate_connection(host, port): 119 | """Checks to see if a connection exists at the current hos and port 120 | 121 | Parameters 122 | ---------- 123 | host : str 124 | host string 125 | port : str 126 | port string 127 | 128 | Returns 129 | ------- 130 | boolean 131 | True for valid connection, False if not 132 | """ 133 | try: 134 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 135 | s.settimeout(1) 136 | s.connect((host, int(port))) 137 | s.close() 138 | except: 139 | return False 140 | return True 141 | 142 | def validate(net_string): 143 | """Runs host/port validation on a string 144 | """ 145 | 146 | host, port = net_string.split(':') 147 | validate_host(host) 148 | validate_port(port) 149 | return (host, port) 150 | 151 | def get_config_file(var): 152 | """Gets the absolute path for a config file associated with a particular 153 | environment variable 154 | """ 155 | return os.path.join(CONFIG_PATH, ENV_TO_CONFIG_FILE[var]) 156 | 157 | def get_zbrush_exec_script(): 158 | """Returns the path to the ZBrush Pre-Exec script if it exists, '' if not. 159 | """ 160 | return os.path.join(CONFIG_PATH, ZBRUSH_PRE_EXEC) 161 | 162 | def get_goz_command_script(): 163 | this_file = os.path.abspath(sys.modules[__name__].__file__) 164 | return os.path.join(os.path.dirname(this_file), 'cmd.py') 165 | 166 | def get_maya_exec_script(): 167 | """Returns the path to the Maya Pre-Exec script if it exists, '' if not. 168 | """ 169 | cfg = os.path.join(CONFIG_PATH, MAYA_PRE_EXEC) 170 | if os.path.exists(cfg): 171 | return cfg 172 | else: 173 | return '' 174 | 175 | def get_shared_dir_config(): 176 | """Returns the path to the config file for the shared_dir 177 | """ 178 | return get_config_file(SHARED_DIR_ENV) 179 | 180 | def get_shared_dir(): 181 | """Returns the string representation of the shared directory. 182 | 183 | First it checks for env variables and if not found, checks the config files 184 | 185 | Returns 186 | ------- 187 | shared_dir : str 188 | """ 189 | # Check for env variable for shared dir 190 | shared_dir = os.getenv(SHARED_DIR_ENV) 191 | if not shared_dir: 192 | # If there's no shared dir, check for the config file 193 | cfg = get_config_file(SHARED_DIR_ENV) 194 | if os.path.exists(cfg): 195 | shared_dir = config_read(SHARED_DIR_ENV) 196 | 197 | # If the shared_dir still doesn't exist, lets use the defaults 198 | if not shared_dir: 199 | if sys.platform == 'darwin': 200 | shared_dir = SHARED_DIR_DEFAULT_OSX 201 | else: 202 | shared_dir = SHARED_DIR_DEFAULT_LINUX 203 | 204 | return shared_dir 205 | 206 | def get_net_info(net_env): 207 | """Gets the net information (host, port) for a given net environment. 208 | 209 | First checks environment variables, then config files, and if 210 | those are empty, it uses the DEFAULT_NET values. 211 | **Missing SHARED_DIR_ENV forces local mode 212 | 213 | Parameters 214 | ---------- 215 | net_env : str 216 | 217 | Returns 218 | ------- 219 | host, port : str 220 | """ 221 | 222 | # check the shared dir first. it could force us into local mode 223 | shared_dir = get_shared_dir() 224 | 225 | # check for empty but existing env var 226 | if shared_dir is '': 227 | shared_dir = None 228 | 229 | if shared_dir is None: 230 | # if no shared directory is set, start in local modee 231 | print "No shared directory set. Defaulting to local mode" 232 | if sys.platform == 'darwin': 233 | print "working on OSX" 234 | os.environ[SHARED_DIR_ENV] = SHARED_DIR_DEFAULT_OSX 235 | elif sys.platform == 'win32' or sys.platform == 'win64': 236 | print "working on Windows" 237 | os.environ[SHARED_DIR_ENV] = SHARED_DIR_DEFAULT_WIN 238 | else: 239 | net_string = os.environ.get(net_env, '') 240 | 241 | if not net_string: 242 | # Check for a config getter 243 | cfg = get_config_file(net_env) 244 | if os.path.exists(cfg): 245 | net_string = config_read(net_env) 246 | 247 | if net_string: 248 | host, port = validate(net_string) 249 | return host, port 250 | 251 | # finally default to local mode 252 | net_string = DEFAULT_NET[net_env] 253 | 254 | if net_string: 255 | host, port = validate(net_string) 256 | return host, port 257 | 258 | def split_file_name(file_path): 259 | """Gets the file 'name' from file, strips ext and dir 260 | """ 261 | file_name = os.path.splitext(file_path)[0] 262 | file_name = os.path.split(file_name)[1] 263 | 264 | return file_name 265 | 266 | def make_maya_filepath(name): 267 | """Makes a full resolved file path for zbrush 268 | """ 269 | return os.path.join(get_shared_dir(), name) + '.ma' 270 | 271 | def send_osa(script_path): 272 | """Sends a zscript file for zbrush to open 273 | """ 274 | cmd = ['osascript -e', 275 | '\'tell app "ZBrush"', 276 | 'to open', 277 | '"' + script_path + '"\''] 278 | 279 | cmd = ' '.join(cmd) 280 | print cmd 281 | os.system(cmd) 282 | 283 | def config_write(var, text): 284 | """Writes the configuration file for the variable specified. 285 | *The variables that can have configuration files are declared in the 286 | utils.ENV_TO_CONFIG_FILE 287 | """ 288 | cfg = get_config_file(var) 289 | if not os.path.exists(os.path.dirname(cfg)): 290 | os.makedirs(os.path.dirname(cfg)) 291 | try: 292 | cfg_write = open(cfg, 'w') 293 | cfg_write.write(text) 294 | finally: 295 | cfg_write.close() 296 | 297 | def config_read(var): 298 | """Reads the configuration file for the variable specified. 299 | *The variables that can have configuration files are declared in the 300 | utils.ENV_TO_CONFIG_FILE 301 | """ 302 | cfg = get_config_file(var) 303 | info = '' 304 | if os.path.exists(cfg): 305 | try: 306 | config = open(cfg, 'r') 307 | info = config.read() 308 | finally: 309 | config.close() 310 | else: 311 | print 'No configuration files have been created yet for %s.' % var 312 | return info 313 | 314 | def open_osa(): 315 | """Opens ZBrush 316 | 317 | blocks untill ZBrush is ready for addional commands 318 | makes sure ZBrush is ready to install the GUI 319 | 320 | launches ZBrush 321 | loop to check if ZBrush is 'ready' 322 | brings ZBrush to front/focus 323 | clears any crash messages 324 | 325 | """ 326 | 327 | osa = "osascript "\ 328 | + "-e 'tell application \"ZBrush\" to launch' "\ 329 | + "-e 'tell application \"System Events\"' "\ 330 | + "-e 'repeat until visible of process \"ZBrushOSX\" is false' "\ 331 | + "-e 'set visible of process \"ZBrushOSX\" to false' "\ 332 | + "-e 'end repeat' "\ 333 | + "-e 'end tell' "\ 334 | + "-e 'tell application \"System Events\"' "\ 335 | + "-e 'tell application process \"ZBrushOSX\"' "\ 336 | + "-e 'set frontmost to true' "\ 337 | + "-e 'keystroke return' "\ 338 | + "-e 'end tell' "\ 339 | + "-e 'end tell'" 340 | 341 | print osa 342 | os.system(osa) 343 | 344 | def force_zbrush_server_close(host=None, port=None): 345 | """Forces the ZBrush server to close under the current configuration 346 | specified in either the environment variables or the config files. If no 347 | parameters are passed in, the one's setup in the configs will be used. 348 | If there are none there, then the defaults will be used. 349 | 350 | Parameters 351 | ---------- 352 | host : str 353 | (optional) host string 354 | port : str 355 | (optional) port string 356 | """ 357 | 358 | if host is None or port is None: 359 | host, port = get_net_info(ZBRUSH_ENV) 360 | print host, port 361 | 362 | if validate_connection(host, port): 363 | try: 364 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 365 | s.settimeout(1) 366 | s.connect((host, int(port))) 367 | s.send('EXIT') 368 | time.sleep(1) 369 | s.close() 370 | except: 371 | print 'The server has been closed!' 372 | 373 | def create_config_path(): 374 | """Creates the configuration file path for the current machine. This needs 375 | to be done for each machine using gozbruh regardless of platform. 376 | 377 | (If environment variables are set for the configuration options, they will 378 | be written for each of the config files needed) 379 | """ 380 | if not os.path.exists(CONFIG_PATH): 381 | print 'Creating gozbruh Configuration Path...' 382 | # Create the config path directory 383 | os.makedirs(CONFIG_PATH) 384 | 385 | # Create blank config files for each of the attributes unless env vars 386 | # are present for the variable, in which case write those. 387 | for key in ENV_TO_CONFIG_FILE: 388 | try: 389 | cfg_write = open(get_config_file(key), 'w') 390 | write_val = os.environ.get(key, '') 391 | cfg_write.write(write_val) 392 | finally: 393 | cfg_write.close() 394 | 395 | def get_zbrush_app_dirs(): 396 | """Returns a list of the ZBrush install locations for OSX 397 | """ 398 | app_dirs = os.listdir('/Applications') 399 | return [d for d in app_dirs if 'ZBrush' in d] 400 | 401 | def install_goz_osx(app_dirs=None): 402 | """Installs the startup scripts for gozbruh installation. Uses the list of dirs 403 | if there's one passed in, otherwise tries to find all of the system's 404 | installations of ZBrush 405 | 406 | Parameters 407 | ---------- 408 | install_dir_list : list of str 409 | (optional) List of all of the ZBrush directories to install to 410 | """ 411 | # Get the ZBrush Application directory 412 | if app_dirs is None: 413 | app_dirs = get_zbrush_app_dirs() 414 | if not app_dirs: 415 | raise Exception('ZBrush not installed in /Applications') 416 | 417 | this_file = os.path.abspath(sys.modules[__name__].__file__) 418 | 419 | for app_dir in app_dirs: 420 | print 'Installing gozbruh for ZBrush in directory %s' % app_dir 421 | default_script = os.path.join(os.path.dirname(this_file), 'DefaultZScript.txt') 422 | script_path = os.path.join('/Applications', app_dir, 'ZScripts') 423 | script_orig = os.path.join(script_path, 'DefaultZScript.zsc') 424 | script_new = os.path.join(script_path, 'DefaultZScript.txt') 425 | 426 | if not os.path.exists(script_new): 427 | # If it does exist, we are going to be creating the new DefaultZScript 428 | # Installation of the new DefaultZScript. Takes place outside 429 | # of Zbrush. 430 | 431 | try: 432 | default_script_read = open(default_script, 'r') 433 | default_script_str = default_script_read.read() 434 | finally: 435 | default_script_read.close() 436 | print 'Installing gozbruh Startup Files for ZBrush...' 437 | 438 | # Write the new DefaultZScript.txt 439 | command_script = get_goz_command_script() 440 | new_script_str = default_script_str.replace('#GOZ_COMMAND_SCRIPT', 441 | command_script) 442 | 443 | try: 444 | script_new_write = open(script_new, 'w') 445 | script_new_write.write(new_script_str) 446 | print 'Wrote ZBrush Startup Script: %s' % script_new 447 | finally: 448 | script_new_write.close() 449 | 450 | def install_goz(app_dir_list=None): 451 | """Essentially an 'install' script for gozbruh. Takes care of setting up the 452 | essentials of what gozbruh needs to run. 453 | """ 454 | if sys.platform == 'darwin': 455 | install_goz_osx(app_dir_list) 456 | 457 | create_config_path() 458 | 459 | print '---------- gozbruh Installation Finished! ----------\n'\ 460 | '\n(Config folder can be found in your home directory %s)' % CONFIG_PATH 461 | -------------------------------------------------------------------------------- /gozbruh/maya_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maya Server and ZBrush client classes 3 | 4 | MayaServer is used to start a commandPort, 5 | and listen for objects from ZBrush 6 | 7 | Objects are loaded when ZBrushServer calls 8 | client.load funcitons with name/path and tool parent 9 | 10 | If the SHARED_DIR_ENV is missing MayaServer/MayaToZBrushClient 11 | will start in a local mode 12 | 13 | MayaToZBrushClient is used for sending ascii files to ZBrush 14 | from Maya, it also manges gozbruhBrushIDs, and gozbruhParent attributes 15 | These attributes are used to keep track of name changes in maya 16 | 17 | Conflicts in the attributes result in renaming on export 18 | or creating new attributes to fit name changes 19 | """ 20 | 21 | import os 22 | 23 | import socket 24 | import errno 25 | 26 | import json 27 | from collections import defaultdict 28 | 29 | import maya.cmds as cmds 30 | import pymel.core as pm 31 | 32 | from . import errs 33 | from . import utils 34 | 35 | # TODO: make this configurable: 36 | # nodes marked for removal from maya on import from ZBrush 37 | GARBAGE_NODES = ['blinn', 38 | 'blinnSG', 39 | 'materialInfo', 40 | 'ZBrushTexture', 41 | 'place2dTexture2'] 42 | 43 | #============================================================================== 44 | # CLASSES 45 | #============================================================================== 46 | 47 | class MayaServer(object): 48 | """Server that uses Maya's built-in commandPort to receive meshes from 49 | zbrush. 50 | 51 | start/stop(host,port) functions open/close the maya commandPort 52 | 53 | Attributes 54 | ---------- 55 | status : bool 56 | current server status (up/down) 57 | host : str 58 | current host for serving on from utils.get_net_info 59 | port : str 60 | current port for serving on from utils.get_net_info 61 | cmdport_name : str 62 | formated command port name (xxx.xxx.xxx.xxx:port) 63 | file_path : str 64 | current file loaded from ZBrush (full path) 65 | file_name : str 66 | current object loaded from ZBrush (name only no ext) 67 | """ 68 | 69 | def __init__(self): 70 | """Initialization: gets networking info, creates command port name 71 | """ 72 | self.host, self.port = utils.get_net_info(utils.MAYA_ENV) 73 | 74 | self.cmdport_name = "%s:%s" % (self.host, self.port) 75 | self.status = False 76 | 77 | def start(self): 78 | """Starts a Maya command port for the host, port specified 79 | """ 80 | 81 | # check network info 82 | utils.validate_host(self.host) 83 | utils.validate_port(self.port) 84 | 85 | self.cmdport_name = "%s:%s" % (self.host, self.port) 86 | self.status = cmds.commandPort(self.cmdport_name, query=True) 87 | 88 | # if down, start a new command port 89 | if self.status is False: 90 | cmds.commandPort(name=self.cmdport_name, sourceType='python') 91 | self.status = cmds.commandPort(self.cmdport_name, query=True) 92 | print 'listening %s' % self.cmdport_name 93 | 94 | def stop(self): 95 | """Stops the maya command port for the host/port specified 96 | """ 97 | cmds.commandPort(name=self.cmdport_name, 98 | sourceType='python', close=True) 99 | self.status = cmds.commandPort(self.cmdport_name, 100 | query=True) 101 | print 'closing %s' % self.cmdport_name 102 | 103 | 104 | class MayaToZBrushClient(object): 105 | """Client used for sending meshes to Zbrush. 106 | 107 | Uses of this class: 108 | Object name management between Zbrush/Maya 109 | Connections to ZBrushServer 110 | Cleaning and exporting mayaAscii files 111 | 112 | Attributes 113 | ---------- 114 | status : bool 115 | status of the connection to ZBrushServer 116 | objs : list of str 117 | list of objects to send to ZBrushServer 118 | host : str 119 | current host obtained from utils.get_net_info 120 | port : str 121 | current port obtained from utils.get_net_info 122 | sock : socket.socket 123 | current open socket connection 124 | 125 | """ 126 | 127 | def __init__(self): 128 | """Gets networking information, initalizes client 129 | """ 130 | 131 | self.host, self.port = utils.get_net_info(utils.ZBRUSH_ENV) 132 | self.status = False 133 | self.sock = None 134 | self.objs = None 135 | self.goz_id = None 136 | self.goz_obj = None 137 | 138 | def connect(self): 139 | """Connect the client to the to ZBrushServer 140 | """ 141 | 142 | try: 143 | # close old socket, might not exist so skip 144 | self.sock.close() 145 | except AttributeError: 146 | print 'no socket to close...' 147 | 148 | self.status = False 149 | 150 | utils.validate_host(self.host) 151 | utils.validate_port(self.port) 152 | 153 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 154 | # time out incase of a bad host/port that actually exists 155 | self.sock.settimeout(45) 156 | 157 | try: 158 | self.sock.connect((self.host, int(self.port))) 159 | except socket.error as err: 160 | self.status = False 161 | if errno.ECONNREFUSED in err: 162 | raise errs.ZBrushServerError( 163 | 'Connection Refused: %s:%s' % (self.host, self.port)) 164 | 165 | self.status = True 166 | 167 | def check_socket(self): 168 | """Verify connection to ZBrushServer 169 | """ 170 | 171 | if self.sock is None: 172 | return 173 | 174 | try: 175 | self.sock.send('check') 176 | if self.sock.recv(1024) == 'ok': 177 | # connected 178 | print 'connected!' 179 | else: 180 | # bad connection, clear socket 181 | self.status = False 182 | self.sock.close() 183 | self.sock = None 184 | print 'conn reset!' 185 | 186 | except socket.error as err: 187 | # catches server down errors, resets socket 188 | self.status = False 189 | self.sock.close() 190 | self.sock = None 191 | if errno.ECONNREFUSED in err: 192 | print 'conn ref' 193 | # server probably down 194 | if errno.EADDRINUSE in err: 195 | # this is fine 196 | print 'already connected...' 197 | if errno.EPIPE in err: 198 | # server down, or unexpected connection interuption 199 | print 'broken pipe, trying to reconnect' 200 | except AttributeError: 201 | print 'need new sock' 202 | 203 | def format_message(self, command, obj_parents): 204 | """Construct a json string to pass to the zbrush server.""" 205 | objData = defaultdict(list) 206 | 207 | for obj, parent in obj_parents: 208 | objData[parent].append(obj) 209 | 210 | return json.dumps({'command': command, 'objData': dict(objData)}) 211 | 212 | def send(self, objs): 213 | """Send a file load command to ZBrush via ZBrushServer. 214 | """ 215 | # export, send 216 | if self.status: 217 | obj_parents = export(objs) 218 | msg = self.format_message('open', obj_parents) 219 | self.sock.send(msg) 220 | # check receipt of objs 221 | self.load_confirm() 222 | else: 223 | raise errs.ZBrushServerError( 224 | 'Please connect to ZBrushServer first') 225 | 226 | def load_confirm(self): 227 | """Check to make sure that sent objects have been loaded after a send. 228 | 'loaded' will be sent back from ZBrushServer 229 | """ 230 | 231 | if self.sock.recv(1024) == 'loaded': 232 | print 'ZBrush Loaded:' 233 | print ('\n'.join(self.objs)) 234 | else: 235 | self.status = False 236 | self.sock = None 237 | print 'ZBrushServer is down!' 238 | raise errs.ZBrushServerError('ZBrushServer is down!') 239 | 240 | #============================================================================== 241 | # FUNCTIONS 242 | #============================================================================== 243 | 244 | def get_goz_objs(): 245 | """Grab meshes from selection, filter out extraneous DAG objects and 246 | freeze transforms on objects. 247 | """ 248 | objs = cmds.ls(selection=True, type='mesh', dag=True) 249 | if objs: 250 | xforms = cmds.listRelatives(objs, parent=True, fullPath=True) 251 | # freeze transform 252 | cmds.makeIdentity(xforms, apply=True, t=1, r=1, s=1, n=0) 253 | cmds.select(xforms) 254 | objs = cmds.ls(selection=True) 255 | return objs 256 | 257 | #------------------------------------------------------------------------------ 258 | # Renaming 259 | #------------------------------------------------------------------------------ 260 | 261 | def handle_renames(objs): 262 | import gozbruh.mayagui as mayagui 263 | # check for any gozbruhBrushIDs, and relink/create 264 | for obj, goz_id in _get_gozid_mismatches(objs[:]): 265 | # relinked objs are removed from self.client.objs 266 | # this prevents relinking 2 previous tool histories 267 | # it stops relinking after the 1st match/relink 268 | # so pSphere1 contains both meshes, but pSphere2 still exists 269 | # this prevents overwriting 2 zbrush tools with the same obj 270 | 271 | # the 'skip' option during in the relink gui keeps the obj to look 272 | # for an alternative history, for example relink the 2nd obj history 273 | # if skip fails to relink, it will default to 'create' 274 | objs = mayagui.rename_prompt(obj, goz_id, objs) 275 | return objs 276 | 277 | def rename_prompt(obj, goz_id, objs): 278 | """Confirm object rename, trigger create or relink then revise 279 | objlist 280 | """ 281 | gui_message = """%s has a old ZBrush ID, of %s, try to relink? 282 | 283 | NOTE! relinking will 284 | remove objects named "%s" 285 | selected mesh as the new one!! 286 | """ % (obj, 287 | goz_id, 288 | goz_id) 289 | 290 | choice = pm.confirmDialog(title="ZBrush Name Conflict", 291 | message=gui_message, 292 | button=['Relink', 'Create', 'Skip']) 293 | if choice == 'Relink': 294 | # relink to past gozbruhBrushID 295 | if obj not in objs: 296 | return 297 | new_obj = relink(obj, goz_id) 298 | objs.remove(obj) 299 | if new_obj not in objs: 300 | objs.append(new_obj) 301 | 302 | elif choice == 'Create': 303 | # new object for zbrush 304 | create(obj) 305 | 306 | def relink(obj, goz_id): 307 | """Relink object name with existing gozbruhBrushID. 308 | """ 309 | # manages re linking gozbruhBrush IDs, checks for attribute on shape/xform 310 | 311 | # in the case of a object being duplicated this removes the duplicate 312 | # to prevent deletion, the 'create' option is prefered 313 | # is only happens when an object was duplicated and merged (original 314 | # still exists) 315 | if cmds.objExists(goz_id): 316 | cmds.delete(goz_id) 317 | 318 | cmds.rename(obj, goz_id) 319 | return create(goz_id) 320 | 321 | def create(obj): 322 | """Tell ZBrush to treat `obj` as a new object. 323 | 324 | Under the hood this changes a gozbruhBrush ID to match object name. 325 | """ 326 | # does not change selection: 327 | cmds.delete(obj, constructionHistory=True) 328 | shape = cmds.ls(obj, type='mesh', dag=True)[0] 329 | xform = cmds.listRelatives(shape, parent=True, fullPath=True)[0] 330 | goz_check_xform = cmds.attributeQuery( 331 | 'gozbruhBrushID', node=xform, exists=True) 332 | goz_check_shape = cmds.attributeQuery( 333 | 'gozbruhBrushID', node=shape, exists=True) 334 | 335 | if goz_check_shape: 336 | cmds.setAttr(shape + '.gozbruhBrushID', obj, type='string') 337 | if goz_check_xform: 338 | cmds.setAttr(xform + '.gozbruhBrushID', obj, type='string') 339 | return xform 340 | 341 | def _get_gozid_mismatches(objs): 342 | """Return objects from `objs` whose gozbruhBrushID does not match their name 343 | 344 | Checks object history for instances of gozbruhBrushID, 345 | returns a list ofgozbruhBrushID/name conflicts 346 | 347 | gozbruhBrushID is created by ZBrush on export and is used to track 348 | name changes that can occur in maya 349 | 350 | this function compares object current name against the ID 351 | and returns a list of conflicts 352 | 353 | this list is handled by the gui to allow for dialog boxes 354 | """ 355 | goz_list = [] 356 | 357 | for obj in objs: 358 | has_attr = cmds.attributeQuery( 359 | 'gozbruhBrushID', node=obj, exists=True) 360 | 361 | if has_attr: 362 | # check for 'rename' 363 | goz_id = cmds.getAttr(obj + '.gozbruhBrushID') 364 | if obj != goz_id: 365 | goz_list.append((obj, goz_id)) 366 | else: 367 | # check for old ID in history 368 | for old_obj in cmds.listHistory(obj): 369 | has_attr = cmds.attributeQuery('gozbruhBrushID', 370 | node=old_obj, 371 | exists=True) 372 | if has_attr: 373 | goz_id = cmds.getAttr(old_obj + '.gozbruhBrushID') 374 | if obj != goz_id: 375 | goz_list.append((obj, goz_id)) 376 | 377 | # resulting mismatches to be handled 378 | return goz_list 379 | 380 | #------------------------------------------------------------------------------ 381 | # Sending / Exporting 382 | #------------------------------------------------------------------------------ 383 | 384 | def export(objs): 385 | """Save files. 386 | 387 | Checks for gozbruhParent attr. 388 | 389 | gozbruhParent is used to import objects in correct order in ZBrush 390 | gozbruhParent determines the top level tool in ZBrush 391 | 392 | If no instance exists, it is created 393 | 394 | Returns 395 | ------- 396 | list of (str, str) 397 | list of object, parent pairs 398 | """ 399 | parents = [] 400 | 401 | for obj in objs: 402 | # export each file individually 403 | cmds.select(cl=True) 404 | cmds.select(obj) 405 | cmds.delete(ch=True) 406 | ascii_path = utils.make_maya_filepath(obj) 407 | cmds.file(ascii_path, 408 | force=True, 409 | options="v=0", 410 | type="mayaAscii", 411 | exportSelected=True) 412 | if cmds.attributeQuery('gozbruhParent', node=obj, exists=True): 413 | # object existed in zbrush, has 'parent' tool 414 | parent = cmds.getAttr(obj + '.gozbruhParent') 415 | # append to the end of parents 416 | parents.append((obj, parent)) 417 | else: 418 | cmds.addAttr(obj, longName='gozbruhParent', dataType='string') 419 | cmds.setAttr(obj + '.gozbruhParent', obj, type='string') 420 | # prepend to the beginning of parents, we want these objects 421 | # imported first 422 | parents = [(obj, obj)] + parents 423 | 424 | # maya is often run as root, this makes sure osx can open/save 425 | # files not needed if maya is run un-privileged 426 | os.chmod(ascii_path, 0o777) 427 | return parents 428 | 429 | def send(client=None): 430 | """Send the current selection in Maya to ZBrush. 431 | 432 | client : `MayaToZBrushClient` 433 | client running in Maya, which can connect to `ZBrushServer` 434 | """ 435 | pre_btn_script = utils.get_maya_exec_script() 436 | # Updates the config files if necessary 437 | if pre_btn_script: 438 | # FIXME: use subprocess 439 | os.system(pre_btn_script + ' False') 440 | 441 | if client is None: 442 | client = MayaToZBrushClient() 443 | 444 | client.check_socket() 445 | 446 | if client.status is False: 447 | # try last socket, or fail 448 | with utils.err_handler(error_gui): 449 | client.connect() 450 | 451 | # construct list of selection, filter meshes 452 | objs = get_goz_objs() 453 | if objs: 454 | objs = handle_renames(objs) 455 | with utils.err_handler(error_gui): 456 | client.send(objs) 457 | else: 458 | error_gui('Please select a mesh to send') 459 | 460 | #------------------------------------------------------------------------------ 461 | # Receiving / Importing 462 | #------------------------------------------------------------------------------ 463 | 464 | def load(file_path, obj_name, parent_name): 465 | """Import a file exported from ZBrush. 466 | 467 | This is the command sent over the Maya command port from ZBrush. 468 | 469 | Parameters 470 | ---------- 471 | file_path : str 472 | Path to the file that we are importing 473 | obj_name : str 474 | Name of the object being imported 475 | parent_name : str 476 | Name of the parent for the object being imported 477 | """ 478 | file_name = utils.split_file_name(file_path) 479 | _cleanup(file_name) 480 | cmds.file(file_path, i=True, 481 | usingNamespaces=False, 482 | removeDuplicateNetworks=True) 483 | 484 | # Set smoothing options if necessary 485 | if cmds.optionVar(ex='gozbruh_smooth') and not cmds.optionVar(q='gozbruh_smooth'): 486 | cmds.displaySmoothness(obj_name, du=0, dv=0, pw=4, ps=1, po=1) 487 | 488 | if not cmds.attributeQuery("gozbruhParent", n=obj_name, ex=True): 489 | cmds.addAttr(obj_name, longName='gozbruhParent', dataType='string') 490 | cmds.setAttr(obj_name + '.gozbruhParent', parent_name, type='string') 491 | 492 | def _cleanup(name): 493 | """Removes un-used nodes on import of obj 494 | """ 495 | 496 | # Don't delete the old mesh if gozbruh_delete option var exists and is set to 497 | # false, simply rename it 498 | if cmds.optionVar(ex='gozbruh_delete') and not cmds.optionVar(q='gozbruh_delete'): 499 | if cmds.objExists(name): 500 | cmds.rename(name, name + '_old') 501 | else: 502 | if cmds.objExists(name): 503 | cmds.delete(name) 504 | 505 | for node in GARBAGE_NODES: 506 | node = name + '_' + node 507 | if cmds.objExists(node): 508 | cmds.delete(node) 509 | 510 | #------------------------------------------------------------------------------ 511 | # Helpers 512 | #------------------------------------------------------------------------------ 513 | 514 | def error_gui(message): 515 | """Simple gui for displaying errors 516 | """ 517 | pm.confirmDialog(title=str('gozbruh Error:'), 518 | message='\n' + str(message), 519 | button=['Ok']) 520 | 521 | # This is unused and exists only for custom integration like a shelf button: 522 | def start_maya_server(): 523 | """Start a server within Maya to listen for meshes from ZBrush, bypassing 524 | `mayagui.Win`. 525 | """ 526 | # pre_btn_script = utils.get_maya_exec_script() 527 | # 528 | # if pre_btn_script: 529 | # import subprocess as sub 530 | # #answer = sub.Popen([pre_btn_script, 'False'], env={'HOME': os.environ['HOME']}, stderr=sub.PIPE) 531 | # answer = sub.call([pre_btn_script, 'False'], env={'HOME': os.environ['HOME']}) 532 | 533 | host, port = utils.get_net_info(utils.MAYA_ENV) 534 | 535 | print 'Starting listen server for gozbruh on %s:%s' % (host, port) 536 | maya = MayaServer() 537 | maya.start() 538 | 539 | # I believe this is also an unused utility: 540 | def send_zbrush(): 541 | """(MAYA FUNCTION) Executes the window for goZ Maya, and then hides it from 542 | the user while proceeding to send the meshes through the UI's 543 | functionality. 544 | """ 545 | from pymel.core import deleteUI 546 | import gozbruh.mayagui as mayagui 547 | 548 | pre_btn_script = utils.get_maya_exec_script() 549 | # Updates the config files if necessary 550 | if pre_btn_script: 551 | os.system(pre_btn_script + ' False') 552 | 553 | # This creates the window that will forge the connection to the zBrush 554 | # machine. 555 | win = mayagui.Win() 556 | # Delete the window 557 | deleteUI(win.gui_window, wnd=True) 558 | # Send the geo 559 | win.send() 560 | -------------------------------------------------------------------------------- /gozbruh/zbrush_tools.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | Starts ZBrushSever, manages ZBrushToMayaClient 4 | 5 | ZbrushServer recived strings such as: 6 | open|objectname#objectparent:anotherobject#anotherparent... 7 | 8 | These are parsed and opened in ZBrush with the use of some apple script 9 | 10 | ZBrushToMayaClient conencts to a open commandPort in maya 11 | 12 | gozbruh.maya_tools.load(file,objname,objparent) is used to open files 13 | """ 14 | import sys 15 | import os 16 | 17 | import socket 18 | import SocketServer 19 | from threading import Thread 20 | 21 | import json 22 | 23 | # FIXME: this should not be necessary 24 | CURRDIR = os.path.dirname(os.path.dirname(os.path.abspath(sys.modules[__name__].__file__))) 25 | sys.path.append(CURRDIR) 26 | from . import utils 27 | 28 | #============================================================================== 29 | # CLASSES 30 | #============================================================================== 31 | 32 | class ZBrushServer(object): 33 | """ZBrush server that gets meshes from Maya. 34 | 35 | Simplifies use of `ZBrushSocketServ`. 36 | 37 | Attributes 38 | ---------- 39 | status : bool 40 | current server status (up/down) 41 | host : str 42 | current host for serving on from utils.get_net_info 43 | port : str 44 | current port for serving on from utils.get_net_info 45 | cmdport_name : str 46 | formated command port name 47 | """ 48 | def __init__(self, host, port): 49 | """Initializes server with host/port to server on, send from 50 | gozbruh.zbrushgui 51 | """ 52 | 53 | self.host = host 54 | self.port = port 55 | self.server = None 56 | self.server_thread = None 57 | self.status = False 58 | 59 | def start(self): 60 | """Looks for previous server, trys to start a new one 61 | """ 62 | 63 | self.status = False 64 | 65 | utils.validate_host(self.host) 66 | utils.validate_port(self.port) 67 | 68 | if self.server is not None: 69 | print 'killing previous server...' 70 | self.server.shutdown() 71 | self.server.server_close() 72 | 73 | print 'starting a new server!' 74 | 75 | self.server = ZBrushSocketServ( 76 | (self.host, int(self.port)), ZBrushHandler) 77 | self.server.allow_reuse_address = True 78 | self.server_thread = Thread(target=self.server.serve_forever) 79 | self.server_thread.daemon = True 80 | self.server_thread.start() 81 | print 'Serving on %s:%s' % (self.host, self.port) 82 | self.status = True 83 | 84 | def stop(self): 85 | """Shuts down ZBrushSever 86 | """ 87 | self.server.shutdown() 88 | self.server.server_close() 89 | print 'stoping...' 90 | self.status = False 91 | 92 | 93 | class ZBrushSocketServ(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 94 | """Extends socket server with custom settings and configures daemon mode 95 | for socketserv module. 96 | """ 97 | timeout = 5 98 | daemon_threads = True 99 | allow_reuse_address = True 100 | 101 | # handler is the RequestHandlerClass 102 | def __init__(self, server_address, handler): 103 | SocketServer.TCPServer.__init__( 104 | self, 105 | server_address, 106 | handler) 107 | 108 | def handle_timeout(self): 109 | print 'TIMEOUT' 110 | 111 | 112 | class ZBrushHandler(SocketServer.BaseRequestHandler): 113 | """Custom handler for ZBrushSever and handles loading objects from maya 114 | 115 | splits the open command: 116 | open|objectname#objectparent:anotherobject#anotherparent... 117 | 118 | Also response with 'loaded' on sucessful object load 119 | 120 | If 'check' is send from MayaToZBrushClient a 'ok' send back 121 | this is used to check if the server is up/ready 122 | 123 | Can be sent 'EXIT' to stop the server thread completely 124 | 125 | """ 126 | 127 | def handle(self): 128 | # keep handle open until client/server close 129 | while True: 130 | data = self.request.recv(1024).strip() 131 | if not data: 132 | self.request.close() 133 | break 134 | 135 | # check for conn-reset/disconnect by peer (on client) 136 | if data == 'check': 137 | self.request.send('ok') 138 | 139 | # Shutdown sequence 140 | if data == 'EXIT': 141 | self.server.server_close() 142 | 143 | # if we get to here then data must be our json string 144 | data = json.loads(data) 145 | 146 | # parse object list from maya 147 | if data.get('command') == 'open': 148 | objData = data.get('objData') 149 | for parent, objs in objData.iteritems(): 150 | for obj in objs: 151 | print 'got: ' + obj 152 | zs_temp = self.get_loader_zscript(obj + '.ma', parent) 153 | utils.send_osa(zs_temp) 154 | print 'loaded all objs!' 155 | self.request.send('loaded') 156 | 157 | @staticmethod 158 | def get_loader_zscript(name, parent): 159 | """Writes a temporary zscript to perform the loading of file `name`. 160 | 161 | The script is saved in CONFIG_PATH/.zbrush/gozbruh/temp/zbrush_load.txt 162 | """ 163 | script_path = os.path.join(utils.CONFIG_PATH, 'temp') 164 | if not os.path.exists(script_path): 165 | os.makedirs(script_path) 166 | script_path = os.path.join(script_path, 'zbrush_load.txt') 167 | zs_temp = open(script_path, 'w+') 168 | 169 | env = utils.get_shared_dir() 170 | print env 171 | 172 | # zbrush script to iterate through sub tools, 173 | # and open matches, appends new tools 174 | 175 | zscript = """ 176 | 177 | //this is a new set of import functions 178 | //it allows the loop up of top level tools 179 | //routine to locate a tool by name 180 | //ZBrush uses ToolID, SubToolID, and UniqueID's 181 | //All of these are realative per project/session 182 | 183 | //find subtool 184 | 185 | [RoutineDef, findSubTool, 186 | 187 | 188 | //iterate through sub tools 189 | //even though the ui element exists 190 | //it may not be visable 191 | [SubToolSelect,0] 192 | 193 | [Loop,[SubToolGetCount], 194 | 195 | //get currently selected tool name to compare 196 | [VarSet,currentTool,[IgetTitle, Tool:Current Tool]] 197 | [VarSet,subTool, [FileNameExtract, #currentTool, 2]] 198 | [If,([StrLength,"#TOOLNAME"]==[StrLength,#subTool])&&([StrFind,#subTool,"#TOOLNAME"]>-1), 199 | //there was a match, import 200 | //stop looking 201 | [LoopExit] 202 | ,] 203 | //move through each sub tool to make it visable 204 | [If,[IsEnabled,Tool:SubTool:SelectDown], 205 | [IPress, Tool:SubTool:SelectDown] 206 | 207 | , 208 | [IPress, Tool:SubTool:Duplicate] 209 | [IPress, Tool:SubTool:MoveDown] 210 | [IPress, Tool:SubTool:All Low] 211 | [IPress, Tool:Geometry:Del Higher] 212 | [LoopExit] 213 | ] 214 | ] 215 | 216 | ] 217 | 218 | 219 | //find parent 220 | 221 | [RoutineDef, findTool, 222 | 223 | //ToolIDs befor 47 are 'default' tools 224 | //48+ are user loaded tools 225 | //this starts the counter at 48 226 | //also gets the last 'tool' 227 | [VarSet,count,[ToolGetCount]-47] 228 | [VarSet,a, 47] 229 | 230 | //flag for if a object was found 231 | //or a new blank object needs to be made 232 | [VarSet, makeTool,0] 233 | 234 | //shuts off interface update 235 | [IFreeze, 236 | 237 | [Loop, #count, 238 | //increment current tool 239 | [VarSet, a, a+1] 240 | 241 | //select tool to look for matches 242 | [ToolSelect, #a] 243 | [SubToolSelect,0] 244 | 245 | //check for matching tool 246 | //looks in the interface/UI 247 | [VarSet, uiResult, [IExists,Tool:SubTool:#PARENT]] 248 | 249 | [If, #uiResult == 1, 250 | //check to see if tool is a parent tool 251 | //if it is select it, otherwise iterate to find sub tool 252 | //ideally direct selection of the subtool would be posible 253 | //but sub tools can potentially be hidden in the UI 254 | //findSubTool iterates through sub tools to find a match 255 | [If, [IExists,Tool:#PARENT], 256 | [IPress, Tool:#PARENT], 257 | ] 258 | 259 | [RoutineCall, findSubTool] 260 | [VarSet, makeTool,0] 261 | [LoopExit] 262 | , 263 | [VarSet,makeTool,1] 264 | 265 | ] 266 | ] 267 | //check to see if found or needs a new blank mesh 268 | [If, #makeTool==1, 269 | //make a blank PolyMesh3D 270 | [ToolSelect, 41] 271 | [IPress,Tool:Make PolyMesh3D] 272 | 273 | , 274 | //otherwise 275 | //find sub tool 276 | 277 | ] 278 | ] 279 | ] 280 | 281 | 282 | //find 'parent tool 283 | //check for sub tool 284 | //if found import 285 | //if missing make new tool 286 | 287 | [RoutineDef, open_file, 288 | //check if in edit mode 289 | [VarSet, ui,[IExists,Tool:SubTool:All Low]] 290 | 291 | //if no open tool make a new tool 292 | // this could happen if there is no active mesh 293 | [If, ui == 0, 294 | [ToolSelect, 41] 295 | [IPress,Tool:Make PolyMesh3D] 296 | , 297 | ] 298 | 299 | //find parent 300 | [RoutineCall, findTool] 301 | 302 | //lowest sub-d 303 | [IPress, Tool:SubTool:All Low] 304 | [FileNameSetNext,"!:#FILENAME"] 305 | //finally import the tool 306 | [IPress,Tool:Import] 307 | ] 308 | 309 | [RoutineCall,open_file] 310 | 311 | """ 312 | 313 | # swap above zscript #'s with info from maya 314 | # then write to temp file 315 | zscript = zscript.replace('#FILENAME', os.path.join(env, name)) 316 | zscript = zscript.replace('#TOOLNAME', name.replace('.ma', '')) 317 | zscript = zscript.replace('#PARENT', parent) 318 | zs_temp.write(zscript) 319 | zs_temp.flush() 320 | zs_temp.close() 321 | return zs_temp.name 322 | 323 | 324 | class ZBrushToMayaClient(object): 325 | """Client that connects to Maya's command port and sends commands to load 326 | exported ZBrush meshes. 327 | 328 | Attributes 329 | ---------- 330 | self.host : str 331 | current host obtained from utils.get_net_info 332 | self.port : str 333 | current port obtained from utils.get_net_info 334 | 335 | Also contains a method to check operation with maya ZBrushToMayaClient.test_client() 336 | 337 | ZBrushToMayaClient.send() is used by the GUI installed in ZBrush by running: 338 | python -m gozbruh.zbrush_tools 339 | 340 | this executes this module as a script with command line arguments 341 | the args contain objectname, and object parent tool 342 | 343 | gozbruh.utils.osa_send is used to create a gui in ZBrush 344 | 345 | gozbruh.utils.osa_open is also used to open ZBrush 346 | 347 | """ 348 | 349 | def __init__(self, host, port): 350 | """ inits client with values from gui""" 351 | self.host = host 352 | self.port = port 353 | 354 | def test_client(self): 355 | """ tests connection with maya, creates a sphere and deletes it """ 356 | 357 | utils.validate_host(self.host) 358 | utils.validate_port(self.port) 359 | 360 | maya_cmd = 'import maya.cmds as cmds;' 361 | maya_cmd += 'cmds.sphere(name="goz_server_test;")' 362 | maya_cmd += 'cmds.delete("goz_server_test")' 363 | maya = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 364 | maya.settimeout(5) 365 | try: 366 | maya.connect((self.host, int(self.port))) 367 | except socket.error as err: 368 | print err 369 | print 'connection refused' 370 | return False 371 | except ValueError: 372 | print 'specify a valid port' 373 | return False 374 | else: 375 | maya.send(maya_cmd) 376 | maya.close() 377 | return True 378 | 379 | @staticmethod 380 | def send(obj_name, parent_name): 381 | """Sends a file to maya 382 | 383 | includes filepath, object name, and the "parent" 384 | 385 | The parent is the top level tool or sub tool 0 of the current tool 386 | this is used to preserve organization when loading back into ZBrush 387 | 388 | connects to maya commandPort and sends the maya commands 389 | 390 | """ 391 | 392 | print 'Parent tool: ' + parent_name 393 | 394 | # construct file read path for maya, uses SHARED_DIR_ENV 395 | # make realative path 396 | file_path = utils.make_maya_filepath(obj_name) 397 | 398 | print file_path 399 | 400 | maya_cmd = 'import gozbruh.maya_tools as maya_tools;maya_tools.load(\'' + \ 401 | file_path + '\',\'' + obj_name + \ 402 | '\',\'' + \ 403 | parent_name + \ 404 | '\')' 405 | 406 | print maya_cmd 407 | 408 | maya_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 409 | 410 | host, port = utils.get_net_info(utils.MAYA_ENV) 411 | 412 | print host, port 413 | 414 | try: 415 | maya_sock.connect((host, int(port))) 416 | except socket.error as err: 417 | print err 418 | print 'connection refused' 419 | except ValueError: 420 | print 'specify a valid port' 421 | else: 422 | maya_sock.send(maya_cmd) 423 | maya_sock.close() 424 | 425 | #============================================================================== 426 | # FUNCTIONS 427 | #============================================================================== 428 | 429 | def start_zbrush_server(): 430 | """Start the server and execute the UI installation for ZBrush. 431 | 432 | Assumes ZBrush is running. 433 | """ 434 | import time 435 | 436 | # Guarentee that the pre-button script has run once before server starts 437 | 438 | # pre_btn_script = utils.get_zbrush_exec_script() 439 | # if pre_btn_script: 440 | # os.system(pre_btn_script + " False") 441 | 442 | host, port = utils.get_net_info(utils.ZBRUSH_ENV) 443 | print host, port 444 | 445 | # Make sure that all previous servers have been shutdown before attempting 446 | # to start a new one! 447 | if utils.validate_connection(host, port): 448 | utils.force_zbrush_server_close() 449 | # Make sure that the server is actually closed before starting the new one 450 | while utils.validate_connection(host, port): 451 | time.sleep(.1) 452 | 453 | # Start the server 454 | 455 | server = ZBrushServer(host, port) 456 | server.start() 457 | 458 | # Now that the server has been started, run the UI script 459 | activate_zscript_ui() 460 | 461 | # Listen loop for the server thread 462 | 463 | while server.server_thread.isAlive(): 464 | time.sleep(1) 465 | 466 | def activate_zbrush(): 467 | """Apple script to open ZBrush and bring to front 468 | """ 469 | utils.open_osa() 470 | 471 | def activate_zscript_ui(): 472 | """Assembles a zscript to be loaded by ZBrush to create GUI buttons. 473 | The config variables are read in and then used. 474 | """ 475 | 476 | # zscript to create the 'send' button 477 | zscript = """ 478 | [RoutineDef, send_file, 479 | 480 | //GET CURRENT ENV VARIABLES FROM THE CONFIG FILE 481 | 482 | 483 | //First execute the script to set/write variables 484 | [VarSet, execScript, "#PRE_EXEC_SCRIPT"] 485 | [VarSet, exists, [FileExists, [StrMerge, "!:", #execScript]]] 486 | [If, exists == 1, 487 | [ShellExecute, #execScript] 488 | ] 489 | 490 | //check if in edit mode 491 | [VarSet, ui,[IExists,Tool:SubTool:All Low]] 492 | 493 | //if no open tool make a new tool 494 | [If, ui == 0, 495 | [ToolSelect, 41] 496 | [IPress,Tool:Make PolyMesh3D] 497 | ,] 498 | 499 | //set lowest subtool resolution 500 | [IPress, Tool:SubTool:All Low] 501 | 502 | //env_path set to the path to the config for shared_dir 503 | [VarSet, env_path, "!:#ENVPATH"] 504 | [MemCreateFromFile, envVarBlock, #env_path] 505 | [MemReadString, envVarBlock, env_path] 506 | [VarSet, env_path, [StrMerge, "!:", env_path, "/"]] 507 | 508 | //extracts the current active tool name 509 | [VarSet, tool_name,[FileNameExtract, [GetActiveToolPath], 2]] 510 | 511 | //appends .ma to the path for export, construct filename 512 | [VarSet, file_name, [StrMerge,tool_name,".ma"]] 513 | 514 | //python module execution command, needs to be abs path 515 | [VarSet, module_path, "/usr/bin/python #GOZ_COMMAND_SCRIPT send "] 516 | 517 | [VarSet, validpath,[FileExists, #env_path]] 518 | 519 | [If, validpath != 1, 520 | 521 | 522 | //prevents zbrush crash from exporting to a invalid path 523 | //if zbrush exports to a bad path it will lock up 524 | [MessageOK, "Invalid ZDOCS file path for export"] 525 | [MessageOK, #env_path] 526 | [Exit] 527 | , 528 | 529 | 530 | ] 531 | 532 | //append env to file path 533 | [VarSet, export_path, [StrMerge,env_path,file_name] ] 534 | 535 | //set the maya 'template?' I think ofer spelled something wrong 536 | //this sets the file name for the next export \w correct 'template' 537 | [FileNameSetNext, #export_path,"ZSTARTUP_ExportTamplates\Maya.ma"] 538 | 539 | //finally export the tool 540 | [IPress,Tool:Export] 541 | 542 | //get base tool 543 | [SubToolSelect,0] 544 | 545 | [VarSet,base_tool,[IgetTitle, Tool:Current Tool]] 546 | [VarSet,base_tool, [FileNameExtract, #base_tool, 2]] 547 | 548 | //trigger the python module to send maya the load commands 549 | 550 | [ShellExecute, 551 | //merge the python command with the tool name 552 | [StrMerge, #module_path, 553 | #tool_name, " ",#base_tool 554 | ] 555 | ] 556 | ] 557 | 558 | //gui button for triggering this script 559 | [IButton, "TOOL:Send to Maya", "Export model as a *.ma to maya", 560 | [RoutineCall, send_file] 561 | ] 562 | 563 | """ 564 | 565 | # zscript to create the 'send -all' button 566 | zscript += """ 567 | [RoutineDef, send_all, 568 | 569 | //First execute the script to set/write variables 570 | [VarSet, execScript, "#PRE_EXEC_SCRIPT"] 571 | [VarSet, exists, [FileExists, [StrMerge, "!:", #execScript]]] 572 | [If, exists == 1, 573 | [ShellExecute, #execScript] 574 | ] 575 | 576 | //check if in edit mode 577 | [VarSet, ui,[IExists,Tool:SubTool:All Low]] 578 | 579 | //if no open tool make a new tool 580 | [If, ui == 0, 581 | [ToolSelect, 41] 582 | [IPress,Tool:Make PolyMesh3D] 583 | ,] 584 | 585 | //set all tools to lowest sub-d 586 | [IPress, Tool:SubTool:All Low] 587 | 588 | //iterator variable 589 | [VarSet,t,0] 590 | 591 | //start at the first subtool 592 | [SubToolSelect,0] 593 | 594 | //iterate through all subtools 595 | [Loop,[SubToolGetCount], 596 | 597 | //increment iterator 598 | [VarSet,t,t+1] 599 | 600 | //select current subtool index in loop 601 | [SubToolSelect,t-1] 602 | 603 | //set base export path #ENVPATH is replace with SHARED_DIR_ENV (expanded) 604 | [VarSet, env_path, "!:#ENVPATH"] 605 | [MemCreateFromFile, envVarBlock, #env_path] 606 | [MemReadString, envVarBlock, env_path] 607 | [VarSet, env_path, [StrMerge, "!:", env_path, "/"]] 608 | 609 | //current tool name 610 | [VarSet, tool_name, [FileNameExtract, [GetActiveToolPath], 2]] 611 | 612 | //start constructing export file path /some/dir/tool.ma 613 | [VarSet, file_name, [StrMerge,tool_name,".ma"]] 614 | 615 | //base python module shell command, needs to be abs path 616 | [VarSet, module_path, "/usr/bin/python #GOZ_COMMAND_SCRIPT send "] 617 | 618 | [VarSet, validpath,[FileExists, #env_path]] 619 | 620 | [If, validpath != 1, 621 | 622 | 623 | //prevents zbrush crash from exporting to a invalid path 624 | //if zbrush exports to a bad path it will lock up 625 | [MessageOK, "Invalid ZDOCS file path for export"] 626 | [MessageOK, #env_path] 627 | [Exit] 628 | , 629 | 630 | 631 | ] 632 | 633 | //full export path 634 | [VarSet, export_path, [StrMerge,env_path,file_name] ] 635 | 636 | //set export path to be used by next command 637 | [FileNameSetNext, #export_path,"ZSTARTUP_ExportTamplates\Maya.ma"] 638 | 639 | //finally export 640 | [IPress,Tool:Export] 641 | 642 | 643 | //get base tool 644 | [SubToolSelect,0] 645 | [VarSet,base_tool,[IgetTitle, Tool:Current Tool]] 646 | [VarSet,base_tool, [FileNameExtract, #base_tool, 2]] 647 | 648 | [ShellExecute, 649 | //join module_path tool_name for maya to load 650 | [StrMerge, #module_path, #tool_name, " ",#base_tool] 651 | ] 652 | ] 653 | ] 654 | [IButton, "TOOL:Send to Maya -all", "Export model as a *.ma to maya", 655 | [RoutineCall, send_all] 656 | ] 657 | """ 658 | 659 | # zscript to create the 'send -vis' button 660 | zscript += """ 661 | [RoutineDef, send_visable, 662 | 663 | //First execute the script to set/write variables 664 | [VarSet, execScript, "#PRE_EXEC_SCRIPT"] 665 | [VarSet, exists, [FileExists, [StrMerge, "!:", #execScript]]] 666 | [If, exists == 1, 667 | [ShellExecute, #execScript] 668 | ] 669 | 670 | //check if in edit mode 671 | [VarSet, ui,[IExists,Tool:SubTool:All Low]] 672 | 673 | //if no open tool make a new tool 674 | [If, ui == 0, 675 | [ToolSelect, 41] 676 | [IPress,Tool:Make PolyMesh3D] 677 | ,] 678 | 679 | //set all tools to lowest sub-d 680 | [IPress, Tool:SubTool:All Low] 681 | 682 | //iterator variable 683 | [VarSet,t,0] 684 | 685 | //start at the first subtool 686 | [SubToolSelect,0] 687 | 688 | //iterate through all subtools 689 | [Loop,[SubToolGetCount], 690 | 691 | //increment iterator 692 | [VarSet,t,t+1] 693 | 694 | //select current subtool index in loop 695 | [SubToolSelect,t-1] 696 | 697 | //set base export path #ENVPATH is replace with SHARED_DIR_ENV (expanded) 698 | [VarSet, env_path, "!:#ENVPATH"] 699 | [MemCreateFromFile, envVarBlock, #env_path] 700 | [MemReadString, envVarBlock, env_path] 701 | [VarSet, env_path, [StrMerge, "!:", env_path, "/"]] 702 | 703 | //current tool name 704 | [VarSet, tool_name, [FileNameExtract, [GetActiveToolPath], 2]] 705 | 706 | //start constructing export file path /some/dir/tool.ma 707 | [VarSet, file_name, [StrMerge,tool_name,".ma"]] 708 | 709 | //base python module shell command, needs to be absolute path 710 | [VarSet, module_path, "/usr/bin/python #GOZ_COMMAND_SCRIPT send "] 711 | 712 | [VarSet, validpath,[FileExists, #env_path]] 713 | 714 | [If, validpath != 1, 715 | 716 | 717 | //prevents zbrush crash from exporting to a invalid path 718 | //if zbrush exports to a bad path it will lock up 719 | [MessageOK, "Invalid ZDOCS file path for export"] 720 | [MessageOK, #env_path] 721 | [Exit] 722 | , 723 | 724 | 725 | ] 726 | 727 | //full export path 728 | [VarSet, export_path, [StrMerge,env_path,file_name] ] 729 | 730 | //set export path to be used by next command 731 | [FileNameSetNext, #export_path,"ZSTARTUP_ExportTamplates\Maya.ma"] 732 | 733 | //check visablility 734 | [VarSet,curTool,[IgetTitle, Tool:Current Tool]] 735 | //look at interface mod 736 | [If,[IModGet,[StrMerge,"Tool:SubTool:",curTool]] >= 16, 737 | //finally export if visable 738 | [IPress,Tool:Export] 739 | 740 | //get base tool 741 | [SubToolSelect,0] 742 | [VarSet,base_tool,[IgetTitle, Tool:Current Tool]] 743 | [VarSet,base_tool, [FileNameExtract, #base_tool, 2]] 744 | 745 | [ShellExecute, 746 | //join module_path tool_name for maya to load 747 | [StrMerge, #module_path, #tool_name, " ",#base_tool] 748 | ] 749 | 750 | , 751 | ] 752 | ] 753 | ] 754 | [IButton, "TOOL:Send to Maya -visible", "Export model as a *.ma to maya", 755 | [RoutineCall, send_visable] 756 | ] 757 | """ 758 | 759 | # Get the path to the file that needs to be exec before button calls if 760 | # it exists. 761 | script_to_exec = utils.get_zbrush_exec_script() 762 | command_script = utils.get_goz_command_script() 763 | 764 | # Create the temp directory if it does not already exist in the config path 765 | script_path = os.path.join(utils.CONFIG_PATH, 'temp') 766 | if not os.path.exists(script_path): 767 | os.makedirs(script_path) 768 | script_path = os.path.join(script_path, 'zbrush_gui.txt') 769 | 770 | env = utils.get_shared_dir_config() 771 | 772 | zscript = zscript.replace('#ENVPATH', env) 773 | zscript = zscript.replace('#GOZ_COMMAND_SCRIPT', command_script) 774 | zscript = zscript.replace('#PRE_EXEC_SCRIPT', script_to_exec) 775 | 776 | try: 777 | zs_temp = open(script_path, 'w+') 778 | zs_temp.write(zscript) 779 | zs_temp.flush() 780 | finally: 781 | zs_temp.close() 782 | 783 | utils.send_osa(script_path) 784 | --------------------------------------------------------------------------------