├── README.md ├── SendPythonCodeToCinema4D ├── SendPythonCodeToCinema4D.pyp └── send_python_code.py ├── npp_example.png ├── pycharm_example.png ├── pycharm_example2.png └── sublime_example.png /README.md: -------------------------------------------------------------------------------- 1 | 2 | # SendPythonCodeToCinema4D 3 | 4 | **SendPythonCodeToCinema4D** is a plugin for **Cinema 4D** that allows you to sent python script code to Cinema 4D and execute it. Plug-in based on Niklas Rosenstein's **Remote Code Runner**. [Get it here.](https://github.com/markkorput/sublime-script) 5 | 6 | ## Installation 7 | 8 | Download the latest code from GitHub and unpack the content into your Cinema 4D plugin directory. 9 | 10 | ``` 11 | Cinema 4D RXX/ 12 | plugins/ 13 | SendPythonCodeToCinema4D/ 14 | SendPythonCodeToCinema4D.pyp 15 | send_python_code.py 16 | 17 | ``` 18 | 19 | ## How to use 20 | 21 | Use command line to send python script code to Cinema 4D: 22 | ``` 23 | cmd python -u "d:\Your Cinema 4D Plugins\SendPythonCode\send_python_code.py" --file "c:\\script.py" 24 | ``` 25 | or use full path to python executable: 26 | ``` 27 | cmd "c:\Python27\python.exe" -u "d:\Your Cinema 4D Plugins\SendPythonCode\send_python_code.py" --file "c:\\script.py" 28 | ``` 29 | 30 | ## Text editor integration 31 | You can use **SendPythonCodeToCinema4D** with different code editors like a Notepad++, Sublime Text or PyCharm. 32 | 33 | ### Notepad++ 34 | 35 | ![Preview Image](npp_example.png) 36 | Open menu **Run** and write: 37 | ``` 38 | python -u "d:\Your Cinema 4D Plugins\SendPythonCode\send_python_code.py" --file "$(FULL_CURRENT_PATH)" --origin "Notepad++" 39 | ``` 40 | Setup run-command name and press **Save**. Now you can send code from opened for editing python script file to Cinema 4D. 41 | Also using Notepad++ **NppExec** plug-in you can send to Cinema 4D *unsaved* code. Install **NppExec** plug-in, open menu **Plugins > NppExec > Execute** and enter this code: 42 | ``` 43 | set TEMP_PATH = $(NPP_DIRECTORY)\temp_script.py 44 | text_saveto "$(TEMP_PATH)" 45 | python -u "d:\Your Cinema 4D Plugins\SendPythonCode\send_python_code.py" --file "$(TEMP_PATH)" --origin "Notepad++" 46 | ``` 47 | ### Sublime Text 48 | ![Preview Image](sublime_example.png) 49 | Open **Tools > Build System > New Build system**. Paste this code: 50 | ```py 51 | { 52 | "cmd": ["python", "-u", "d:\\Your Cinema 4D Plugins\\SendPythonCode\\send_python_code.py", "--file", "$file", "--origin","Sublime Text"], 53 | "selector": "source.python", 54 | "file_regex": "^\\s*File \"(...*?)\", line ([0-9]*)" 55 | } 56 | ``` 57 | Save file with desired name. Then go to **Tools > Build System** and select already created Build system. To send code to Cinema 4D use command **Tools > Build**. Also for more convienient use you can setup shortkey. 58 | 59 | ### PyCharm 60 | ![Preview Image](pycharm_example.png) 61 | Go to menu **File > Settings > Tools > External Tools**. 62 | Setup Tool settings like this: 63 | ``` 64 | Name: SendCodeToC4D 65 | Description: Send python code to Cinema 4D from PyCharm 66 | Program: python (or c:\Python27\python.exe) 67 | Arguments: -u "d:\Your Cinema 4D Plugins\SendPythonCode\send_python_code.py" --file $FilePath$ --origin PyCharm 68 | Working directory: $FileDir$ 69 | ``` 70 | To send code to Cinema 4D use command **Tools > External tools > SendCodeToC4D**. Also for more convienient use you can setup shortkey. 71 | 72 | ![Preview Image](pycharm_example2.png) 73 | 74 | ## Advanced features 75 | You can use **SendPythonCodeToCinema4D** also for editing Cinema 4D Python objects like a *Python Generator*, *Python Effector*, *Python Tag* and *Python Field*. For that use script code docstring: 76 | 77 | To change code in **Python generator** use in docstring: 78 | ```c 79 | Generator: 80 | ``` 81 | To change code in **Python effector** use in docstring: 82 | ```c 83 | Effector: 84 | ``` 85 | To change code in **Python tag** use in docstring: 86 | ```c 87 | Tag: 88 | ``` 89 | To change code in **Python field** use in docstring: 90 | ```c 91 | Field: 92 | ``` 93 | 94 | ### Example: 95 | After sending this code to scene all Python field objects with name "Python Field" will recive it. 96 | ```py 97 | """ 98 | Python Field description 99 | 100 | Author : Mike Udin 101 | Web-site : mikeudin.net 102 | Field : Python Field 103 | 104 | """ 105 | import c4d 106 | 107 | def main(): 108 | # code here 109 | pass 110 | ``` 111 | 112 | [mikeudin.net](https://mikeudin.net/) 113 | 114 | 115 | -------------------------------------------------------------------------------- /SendPythonCodeToCinema4D/SendPythonCodeToCinema4D.pyp: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # 3 | # Copyright (C) 2014 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | __author__ = 'Niklas Rosenstein ' 24 | __version__ = '1.1' 25 | 26 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | # Shared Code (SocketFile wrapper class) 28 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | import sys 31 | if sys.version_info[0] < 3: 32 | try: from io import StringIO as BytesIO 33 | except ImportError: from io import StringIO as BytesIO 34 | else: 35 | from io import BytesIO 36 | 37 | class SocketFile(object): 38 | """ 39 | File-like wrapper for reading socket objects. 40 | """ 41 | 42 | def __init__(self, socket, encoding=None): 43 | super(SocketFile, self).__init__() 44 | self._socket = socket 45 | self._buffer = BytesIO() 46 | self.encoding = encoding 47 | 48 | def _append_buffer(self, data): 49 | pos = self._buffer.tell() 50 | self._buffer.seek(0, 2) 51 | self._buffer.write(data) 52 | self._buffer.seek(pos) 53 | 54 | def bind(self, *args, **kwargs): 55 | return self._socket.bind(*args, **kwargs) 56 | 57 | def connect(self, *args, **kwargs): 58 | return self._socket.connect(*args, **kwargs) 59 | 60 | def read(self, length, blocking=True): 61 | data = self._buffer.read(length) 62 | delta = length - len(data) 63 | if delta > 0: 64 | self._socket.setblocking(blocking) 65 | try: 66 | data += self._socket.recv(delta) 67 | except socket.error: 68 | pass 69 | return data 70 | 71 | def readline(self): 72 | parts = [] 73 | while True: 74 | # Read the waiting data from the socket. 75 | data = self.read(1024, blocking=False) 76 | 77 | # If it contains a line-feed character, we add it 78 | # to the result list and append the rest of the data 79 | # to the buffer. 80 | if b'\n' in data: 81 | left, right = data.split(b'\n', 1) 82 | parts.append(left + b'\n') 83 | self._append_buffer(right) 84 | break 85 | 86 | else: 87 | if data: 88 | parts.append(data) 89 | 90 | # Read a blocking byte for which we will get an empty 91 | # bytes object if the socket is closed- 92 | byte = self.read(1, blocking=True) 93 | if not byte: 94 | break 95 | 96 | # Add the byte to the buffer. Stop here if it is a 97 | # newline character. 98 | parts.append(byte) 99 | if byte == b'\n': 100 | break 101 | 102 | return b''.join(parts) 103 | 104 | def write(self, data): 105 | if isinstance(data, str): 106 | if not self.encoding: 107 | raise ValueError('got str object and no encoding specified') 108 | data = data.encode(self.encoding) 109 | 110 | return self._socket.send(data) 111 | 112 | def close(self): 113 | return self._socket.close() 114 | 115 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 116 | # Request Handling and Server thread 117 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | import sys, codecs, socket, hashlib, threading, os 120 | 121 | class SourceObject(object): 122 | """ 123 | Represents source-code sent over from another machine or 124 | process which can be executed later. 125 | """ 126 | 127 | def __init__(self, addr, filename, source, origin): 128 | super(SourceObject, self).__init__() 129 | self.host, self.port = addr 130 | self.filename = filename 131 | self.source = source 132 | self.origin = origin 133 | 134 | def __repr__(self): 135 | return ''.format( 136 | self.filename, self.origin, self.host, self.port) 137 | 138 | def execute(self, scope): 139 | """ 140 | Execute the source in the specified scope. 141 | """ 142 | 143 | code = compile(self.source, self.filename, 'exec') 144 | exec(code, scope) 145 | 146 | def parse_headers(fp): 147 | """ 148 | Parses HTTP-like headers into a dictionary until an empty line 149 | is found. Invalid headers are ignored and if a header is found 150 | twice, it won't overwrite its previous value. Header-keys are 151 | converted to lower-case and stripped of whitespace at both ends. 152 | """ 153 | 154 | headers = {} 155 | while True: 156 | line = fp.readline().strip() 157 | if not line: break 158 | 159 | if sys.version_info[0] < 3: 160 | key, _, value = line.partition(':') 161 | else: 162 | key, _, value = line.decode().partition(':') 163 | 164 | key = key.rstrip().lower() 165 | if key not in headers: 166 | headers[key] = value.lstrip() 167 | 168 | return headers 169 | 170 | def parse_request(conn, addr, required_password): 171 | """ 172 | Communicates with the client parsing the headers and source 173 | code that is to be queued to be executed any time soon. 174 | 175 | Writes on of these lines back to the client: 176 | 177 | - status: invalid-password 178 | - status: invalid-request 179 | - status: encoding-error 180 | - status: ok 181 | 182 | :pass conn: The socket to the client. 183 | :pass addr: The client address tuple. 184 | :pass required_password: The password that must match the 185 | password sent with the "Password" header (as encoded 186 | utf8 converted to md5). Will be converted to md5 by 187 | this function. 188 | :return: :class:`SourceObject` or None 189 | """ 190 | 191 | client = SocketFile(conn, encoding='utf8') 192 | headers = parse_headers(client) 193 | # print headers 194 | # Get the password and validate it. 195 | if required_password is not None: 196 | passhash = hashlib.md5(required_password.encode('utf8')).hexdigest() 197 | if passhash != headers['password']: 198 | client.write('status: invalid-password') 199 | return None 200 | 201 | # Get the content-length of the request. 202 | content_length = headers.get('content-length', None) 203 | if content_length is None: 204 | client.write('status: invalid-request') 205 | return None 206 | try: 207 | content_length = int(content_length) 208 | except ValueError as exc: 209 | client.write('status: invalid-request') 210 | return None 211 | 212 | # Get the encoding, default to binary. 213 | encoding = headers.get('encoding', None) 214 | if encoding is None: 215 | encoding = 'binary' 216 | else: 217 | # default to binary if the encoding does not exist. 218 | try: codecs.lookup(encoding) 219 | except LookupError as exc: 220 | encoding = 'binary' 221 | 222 | # Get the filename, origin and source code. 223 | origin = headers.get('origin', 'unknown') 224 | filename = headers.get('filename', 'untitled') 225 | try: 226 | source = client.read(content_length) 227 | if encoding != 'binary': 228 | source = source.decode(encoding) 229 | except UnicodeDecodeError as exc: 230 | client.write('status: encoding-error') 231 | return None 232 | 233 | client.write('status: ok') 234 | return SourceObject(addr, filename, source, origin) 235 | 236 | 237 | def get_module_docstring(filepath): 238 | "Get module-level docstring of Python module at filepath, e.g. 'path/to/file.py'." 239 | co = compile(open(filepath).read(), filepath, 'exec') 240 | if co.co_consts and isinstance(co.co_consts[0], str): 241 | docstring = co.co_consts[0] 242 | else: 243 | docstring = None 244 | return docstring 245 | 246 | def no_recur_iter(obj): # no recursion hierachy iteration 247 | 248 | op = obj 249 | 250 | while op: 251 | yield op 252 | 253 | if op.GetDown(): 254 | op = op.GetDown() 255 | continue 256 | 257 | while not op.GetNext() and op.GetUp(): 258 | op = op.GetUp() 259 | 260 | #if op == obj: break 261 | 262 | op = op.GetNext() 263 | 264 | class ServerThread(threading.Thread): 265 | """ 266 | When the thread is started, the thread binds a server to the 267 | specified host and port accepting incoming source code, optionally 268 | password protected, and appends it to the specified queue. A lock 269 | for synchronization must be passed along with the queue. 270 | """ 271 | 272 | def __init__(self, queue, queue_lock, host, port, password=None): 273 | super(ServerThread, self).__init__() 274 | self._queue = queue 275 | self._queue_lock = queue_lock 276 | self._socket = None 277 | self._addr = (host, port) 278 | self._running = False 279 | self._lock = threading.Lock() 280 | self._password = password 281 | 282 | @property 283 | def running(self): 284 | with self._lock: 285 | return self._running 286 | 287 | @running.setter 288 | def running(self, value): 289 | with self._lock: 290 | self._running = value 291 | 292 | def run(self): 293 | try: 294 | while self.running: 295 | source = self.handle_request() 296 | if source: 297 | with self._queue_lock: 298 | self._queue.append(source) 299 | finally: 300 | self._socket.close() 301 | 302 | def start(self): 303 | self._socket = socket.socket() 304 | self._socket.bind(self._addr) 305 | self._socket.listen(5) 306 | self._socket.settimeout(0.5) 307 | self.running = True 308 | return super(ServerThread, self).start() 309 | 310 | def handle_request(self): 311 | conn = None 312 | try: 313 | conn, addr = self._socket.accept() 314 | source = parse_request(conn, addr, self._password) 315 | return source 316 | except socket.timeout: 317 | return None 318 | finally: 319 | if conn: 320 | conn.close() 321 | 322 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 323 | # Cinema 4D integration 324 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 325 | 326 | import c4d, collections, traceback 327 | 328 | plugins = [] 329 | 330 | class CodeExecuterMessageHandler(c4d.plugins.MessageData): 331 | 332 | PLUGIN_ID = 1033731 333 | PLUGIN_NAME = "Remote Code Executor" 334 | 335 | def __init__(self, host, port, password): 336 | super(CodeExecuterMessageHandler, self).__init__() 337 | self.queue = collections.deque() 338 | self.queue_lock = threading.Lock() 339 | self.thread = ServerThread(self.queue, self.queue_lock, host, port, password) 340 | 341 | print("Binding Remote Code Executor Server to {0}:{1} ...".format(host, port)) 342 | try: 343 | self.thread.start() 344 | except socket.error as exc: 345 | print("Failed to bind to {0}:{1}\n{2}".format(host, port,exc)) 346 | self.thread = None 347 | 348 | def register(self): 349 | return c4d.plugins.RegisterMessagePlugin( 350 | self.PLUGIN_ID, self.PLUGIN_NAME, 0, self) 351 | 352 | def get_scope(self): 353 | doc = c4d.documents.GetActiveDocument() 354 | op = doc.GetActiveObject() 355 | mat = doc.GetActiveMaterial() 356 | tp = doc.GetParticleSystem() 357 | return { 358 | '__name__': '__main__', 359 | 'doc': doc, 'op': op, 360 | 'mat': mat, 'tp': tp 361 | } 362 | 363 | def on_shutdown(self): 364 | if self.thread: 365 | print("Shutting down Remote Code Executor Server thread ...") 366 | self.thread.running = False 367 | self.thread.join() 368 | self.thread = None 369 | 370 | def GetTimer(self): 371 | if self.thread: 372 | return 500 373 | return 0 374 | 375 | def CoreMessage(self, kind, bc): 376 | # Execute source code objects while they're available. 377 | while True: 378 | with self.queue_lock: 379 | if not self.queue: break 380 | source = self.queue.popleft() 381 | try: 382 | scope = self.get_scope() 383 | scope['__file__'] = source.filename 384 | if not obj_execute(source): 385 | print("RemoteCodeRunner: running", source) 386 | source.execute(scope) 387 | except Exception as exc: 388 | traceback.print_exc() 389 | return True 390 | 391 | def obj_execute(source): 392 | 393 | code = source.source 394 | mode = py_name = '' 395 | scriptdoc = get_module_docstring(source.filename) 396 | 397 | if scriptdoc: 398 | for line in scriptdoc.splitlines(): 399 | 400 | try: 401 | mode,py_name = line.split(':') 402 | mode = mode.strip() #Delete spaces around 403 | py_name = py_name.strip() 404 | except: 405 | pass 406 | 407 | if mode in ['Generator','Effector','Tag','Field']: 408 | break 409 | 410 | if mode not in ['Generator','Effector','Tag','Field'] or not py_name: 411 | return False 412 | 413 | doc = c4d.documents.GetActiveDocument() 414 | 415 | # Searching Python objects or tags 416 | counter = 0 417 | for obj in no_recur_iter(doc.GetFirstObject()): 418 | # print obj 419 | if mode == 'Generator' and obj.GetType() == 1023866 and obj.GetName() == py_name: 420 | obj[c4d.OPYTHON_CODE] = str(code) if sys.version_info[0] < 3 else str(code,'utf-8') 421 | counter += 1 422 | 423 | if mode == 'Effector' and obj.GetType() == 1025800 and obj.GetName() == py_name: 424 | obj[c4d.OEPYTHON_STRING] = str(code) if sys.version_info[0] < 3 else str(code,'utf-8') 425 | counter += 1 426 | 427 | if mode == 'Field' and obj.GetType() == 440000277 and obj.GetName() == py_name: 428 | obj[c4d.PYTHON_CODE] = str(code) if sys.version_info[0] < 3 else str(code,'utf-8') 429 | counter += 1 430 | 431 | if mode == 'Tag': 432 | tags = [t for t in obj.GetTags() if t.GetType() == 1022749] 433 | # print tags 434 | for tag in tags: 435 | if tag.GetName() == py_name: 436 | tag[c4d.TPYTHON_CODE] = str(code) if sys.version_info[0] < 3 else str(code,'utf-8') 437 | counter += 1 438 | 439 | print('RemoteCodeRunner: code was changed in {0} {1}s '.format(counter, mode)) 440 | c4d.EventAdd() 441 | 442 | return True 443 | 444 | def main(): 445 | global plugins 446 | handler = CodeExecuterMessageHandler('localhost', 2900, 'alpine') 447 | handler.register() 448 | plugins.append(handler) 449 | 450 | def PluginMessage(kind, data): 451 | if kind in [c4d.C4DPL_ENDACTIVITY, c4d.C4DPL_RELOADPYTHONPLUGINS]: 452 | for plugin in plugins: 453 | method = getattr(plugin, 'on_shutdown', None) 454 | if callable(method): 455 | try: method() 456 | except Exception as exc: 457 | traceback.print_exc() 458 | return True 459 | 460 | if __name__ == "__main__": 461 | main() 462 | 463 | -------------------------------------------------------------------------------- /SendPythonCodeToCinema4D/send_python_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # 3 | # Copyright (C) 2014 Niklas Rosenstein 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | __author__ = 'Niklas Rosenstein , Mike Udin ' 24 | __version__ = '1.5' 25 | 26 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | # Shared Code (SocketFile wrapper class) 28 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | import sys 31 | import argparse 32 | import code 33 | from os.path import exists 34 | 35 | if sys.version_info[0] < 3: 36 | try: from cStringIO import StringIO as BytesIO 37 | except ImportError: from StringIO import StringIO as BytesIO 38 | else: 39 | from io import BytesIO 40 | 41 | class SocketFile(object): 42 | """ 43 | File-like wrapper for reading socket objects. 44 | """ 45 | 46 | def __init__(self, socket, encoding=None): 47 | super(SocketFile, self).__init__() 48 | self._socket = socket 49 | self._buffer = BytesIO() 50 | self.encoding = encoding 51 | 52 | def _append_buffer(self, data): 53 | pos = self._buffer.tell() 54 | self._buffer.seek(0, 2) 55 | self._buffer.write(data) 56 | self._buffer.seek(pos) 57 | 58 | def bind(self, *args, **kwargs): 59 | return self._socket.bind(*args, **kwargs) 60 | 61 | def connect(self, *args, **kwargs): 62 | return self._socket.connect(*args, **kwargs) 63 | 64 | def read(self, length, blocking=True): 65 | data = self._buffer.read(length) 66 | delta = length - len(data) 67 | if delta > 0: 68 | self._socket.setblocking(blocking) 69 | try: 70 | data += self._socket.recv(delta) 71 | except socket.error: 72 | pass 73 | return data 74 | 75 | def readline(self): 76 | parts = [] 77 | while True: 78 | # Read the waiting data from the socket. 79 | data = self.read(1024, blocking=False) 80 | 81 | # If it contains a line-feed character, we add it 82 | # to the result list and append the rest of the data 83 | # to the buffer. 84 | if b'\n' in data: 85 | left, right = data.split(b'\n', 1) 86 | parts.append(left + b'\n') 87 | self._append_buffer(right) 88 | break 89 | 90 | else: 91 | if data: 92 | parts.append(data) 93 | 94 | # Read a blocking byte for which we will get an empty 95 | # bytes object if the socket is closed- 96 | byte = self.read(1, blocking=True) 97 | if not byte: 98 | break 99 | 100 | # Add the byte to the buffer. Stop here if it is a 101 | # newline character. 102 | parts.append(byte) 103 | if byte == b'\n': 104 | break 105 | 106 | return b''.join(parts) 107 | 108 | def write(self, data): 109 | # if isinstance(data, str): 110 | # if not self.encoding: 111 | # raise ValueError('got str object and no encoding specified') 112 | # data = data.encode(self.encoding) 113 | 114 | return self._socket.send(data.encode()) 115 | 116 | def close(self): 117 | return self._socket.close() 118 | 119 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | # Communication with code reciever server 121 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 122 | 123 | import socket 124 | import hashlib 125 | 126 | def send_code(filename, code, encoding, password, host, port, origin): 127 | """ 128 | Sends Python code to Cinema 4D running at the specified location 129 | using the supplied password. 130 | 131 | :raise ConnectionRefusedError: 132 | """ 133 | 134 | client = SocketFile(socket.socket()) 135 | client.connect((host, port)) #! ConnectionRefusedError 136 | 137 | # print encoding 138 | # if isinstance(code, str): 139 | # code = code.encode(encoding) 140 | 141 | client.encoding = 'ascii' 142 | client.write("Content-length: {0}\n".format(len(code))) 143 | # The Python instance on the other end will check for a coding 144 | # declaration or otherwise raise a SyntaxError if an invalid 145 | # character was found. 146 | client.write("Encoding: binary\n") 147 | client.write("Filename: {0}\n".format(filename)) 148 | client.write("Origin: {0}\n".format(origin)) 149 | 150 | if password: 151 | passhash = hashlib.md5(password.encode('utf8')).hexdigest() 152 | client.write("Password: {0}\n".format(passhash)) 153 | client.write('\n') # end headers 154 | 155 | client.encoding = encoding 156 | client.write(code) 157 | 158 | # Read the response from the server. 159 | result = client.readline().decode('ascii') 160 | client.close() 161 | 162 | status = result.partition(':')[2].strip() 163 | if status == 'ok': 164 | return None 165 | return status # error code 166 | 167 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 168 | # Code Editor Integration 169 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 170 | 171 | import traceback 172 | 173 | class SendPythonCodeCommand(): 174 | 175 | def __init__(self,pars): 176 | self.pars = pars 177 | 178 | def run(self): 179 | 180 | # view = sublime.active_window().active_view() 181 | filename = self.pars['file'] 182 | 183 | if not exists(filename): 184 | print ("File \'{0}\' is not exist.".format(filename)) 185 | return 186 | 187 | 188 | code = open(filename, "r").read() 189 | 190 | if sys.version_info[0] < 3: 191 | encoding = 'UTF-8' 192 | try: 193 | decoded = code.decode('UTF-8') 194 | except UnicodeDecodeError: 195 | encoding = 'ascii' 196 | else: 197 | for ch in decoded: 198 | if 0xD800 <= ord(ch) <= 0xDFFF: 199 | encoding = 'ascii' 200 | # encoding = 'UTF-8' 201 | else: 202 | encoding = 'UTF-8' 203 | # if encoding == 'Undefined': 204 | # encoding = 'UTF-8' 205 | 206 | try: 207 | password, host, port, origin = self.pars['password'],self.pars['host'],self.pars['port'],self.pars['origin'] 208 | except ValueError as exc: 209 | print('Invalid credentials.') 210 | return 211 | 212 | try: 213 | error = send_code(filename, code, encoding, password, host, port, str(origin)) 214 | # except ConnectionRefusedError as exc: 215 | # print('Could not connect to {0}:{1}'.format(host, port)) 216 | # return 217 | except socket.error as exc: 218 | print('socket.error occured, see console') 219 | # show_console() 220 | traceback.print_exc() 221 | return 222 | except UnicodeDecodeError: 223 | print('UnicodeDecodeError occured, see console') 224 | traceback.print_exc() 225 | return 226 | 227 | if error == 'invalid-password': 228 | print('Password was not accepted by the Remote Code Executor Server at {0}:{1}'.format(host, port)) 229 | elif error == 'invalid-request': 230 | print('Request was invalid, maybe this plugin is outdated?') 231 | elif error is not None: 232 | print('error (unexpected): {0}'.format(error)) 233 | else: 234 | print('Code sent to {0}:{1}'.format(host, port)) 235 | 236 | 237 | def main(): 238 | 239 | parser = argparse.ArgumentParser(description= 'This script sends Python code to Cinema 4D running at the specified location using the supplied password.') 240 | 241 | parser.add_argument('--file', type=str, help='Editing python code file.',required=True) 242 | parser.add_argument('--password', type=str, help='Socket Password',default='alpine') 243 | parser.add_argument('--port', type=int, help='Socket Port',default=2900) 244 | parser.add_argument('--host', type=str, help='Socket Host',default='localhost') 245 | parser.add_argument('--origin', type=str, help='Python code source application name',default='PythonEditor') 246 | 247 | # parser.print_help() 248 | 249 | args = parser.parse_args() 250 | # print vars(args) 251 | 252 | py_command = SendPythonCodeCommand(vars(args)) 253 | py_command.run() 254 | 255 | 256 | if __name__ == '__main__': 257 | main() 258 | -------------------------------------------------------------------------------- /npp_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeUdin/SendPythonCodeToCinema4D/c1f8d9251efb286db4ef478058d5a633452d038e/npp_example.png -------------------------------------------------------------------------------- /pycharm_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeUdin/SendPythonCodeToCinema4D/c1f8d9251efb286db4ef478058d5a633452d038e/pycharm_example.png -------------------------------------------------------------------------------- /pycharm_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeUdin/SendPythonCodeToCinema4D/c1f8d9251efb286db4ef478058d5a633452d038e/pycharm_example2.png -------------------------------------------------------------------------------- /sublime_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeUdin/SendPythonCodeToCinema4D/c1f8d9251efb286db4ef478058d5a633452d038e/sublime_example.png --------------------------------------------------------------------------------