├── README.md ├── blender-websocket.js ├── usage.html └── websocket_server.py /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket server for Blender 2 | 3 | This project includes a Blender add-on (server) and a JavaScript library (client). 4 | 5 | The server sends its state to the clients, and can receive a few commands to edit Blender remotely. 6 | 7 | ## Blender 8 | 9 | 1. Download [WebSocket-for-Python](https://github.com/Lawouach/WebSocket-for-Python/archive/master.zip) 10 | 2. Copy the directory `ws4py` into `/2.xx/python/lib` 11 | 3. Download `websocket_server.py` 12 | 4. in Blender, go to `File` > `User Preferences...` > `Add-ons` > `Install from file...` and select the file 13 | 5. Enable the add-on by ticking the corresponding box 14 | 15 | Preferences are shown by expanding the add-on box. In particular, you can choose which data to send to the clients, in order to lower communication for unused data. 16 | 17 | ## JavaScript 18 | 19 | Include the library as usual in your HTML. 20 | 21 | 22 | 23 | ### BlenderWebSocket() 24 | 25 | blender = new BlenderWebSocket(); 26 | 27 | #### .close() 28 | 29 | #### .addListener(event, handler) 30 | 31 | Alias: `on` 32 | 33 | #### .open([options]) 34 | 35 | Default options: 36 | 37 | * `url`: `ws://localhost:8137` 38 | 39 | #### .removeListener(event, handler) 40 | 41 | Alias: `off` 42 | 43 | #### .setAxes(axes) 44 | 45 | Specifies client-side axis permutation. 46 | 47 | In Blender, the coordinate system is right-handed and Z is vertical, but you may want another axis depending on your rendering engine. **Only applies on objects' location and scale, not rotation.** 48 | 49 | The `axes` parameter is a string (up to 3 characters) containing the mapping for each axis. The first character is the mapping for the resulting X axis, and so on. The character could either be `x`, `y`, `z` (positive) or `X`, `Y`, `Z` (negative). 50 | 51 | Example: if you want a right-handed coordinate system and Y as vertical axis, call 52 | 53 | blender.setAxes("xzY") 54 | 55 | #### .setContext(context) 56 | 57 | #### .setData(data) 58 | 59 | #### .setScene(scene, diff) 60 | 61 | ### Events 62 | 63 | ## License 64 | 65 | Copyright (c) 2015 Bloutiouf aka Jonathan Giroux 66 | 67 | [MIT License](http://opensource.org/licenses/MIT) 68 | -------------------------------------------------------------------------------- /blender-websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * WebSocket server for Blender 3 | * Version 0.1.0 4 | * Copyright 2015 Jonathan Giroux (Bloutiouf) 5 | * Licensed under MIT (http://opensource.org/licenses/MIT) 6 | */ 7 | 8 | BlenderWebSocket = (function() { 9 | function isObject(input) { 10 | return (Object.prototype.toString.call(input) === "[object Object]"); 11 | }; 12 | 13 | // recursive 14 | function merge(target) { 15 | for (var i = 1, n = arguments.length; i < n; ++i) { 16 | var arg = arguments[i]; 17 | if (!isObject(arg)) continue; 18 | for (var prop in arg) { 19 | if (isObject(target[prop]) && isObject(arg[prop])) 20 | merge(target[prop], arg[prop]); 21 | else 22 | target[prop] = arg[prop]; 23 | } 24 | } 25 | return target; 26 | } 27 | 28 | function BlenderWebSocket() { 29 | this.context = {}; 30 | this.data = {}; 31 | this.scenes = {}; 32 | 33 | this.axes = new Array(3); 34 | this.connected = false; 35 | this.listeners = {}; 36 | 37 | this.setAxes(""); // default axes 38 | } 39 | 40 | BlenderWebSocket.prototype.addListener = BlenderWebSocket.prototype.on = function(event, handler) { 41 | if (!this.listeners[event]) 42 | this.listeners[event] = [handler]; 43 | else 44 | this.listeners[event].push(handler); 45 | }; 46 | 47 | BlenderWebSocket.prototype.close = function() { 48 | if (this.websocket) 49 | this.websocket.close(); // cleared in onclose 50 | }; 51 | 52 | BlenderWebSocket.prototype.open = function(options) { 53 | if (this.websocket) 54 | return; 55 | 56 | var self = this; 57 | 58 | options = merge({ 59 | url: "ws://localhost:8137/" 60 | }, options); 61 | 62 | this.context = {}; 63 | this.data = {}; 64 | this.scenes = {}; 65 | 66 | var websocket = this.websocket = new WebSocket(options.url); 67 | 68 | var listeners = this.listeners; 69 | 70 | function emit(event) { 71 | var handlers = listeners[event]; 72 | if (handlers) { 73 | var args = Array.prototype.slice.call(arguments, 1); 74 | handlers.forEach(function(handler) { 75 | handler.apply(null, args); 76 | }); 77 | } 78 | } 79 | 80 | websocket.onclose = function() { 81 | if (self.connected) 82 | emit("close"); 83 | self.connected = false; 84 | self.websocket = null; 85 | }; 86 | 87 | websocket.onerror = emit.bind(this, "error"); 88 | 89 | var axes = this.axes; 90 | 91 | function swapAxes(arr, offset) { 92 | offset = offset || 0; 93 | return arr.map(function swapAxesIter(v, i) { 94 | if (i < offset) 95 | return v; 96 | var axis = axes[i - offset]; 97 | return arr[axis.index] * axis.scale; 98 | }); 99 | } 100 | 101 | function swapAxesData(data) { 102 | if (data.objects) { 103 | for (var name in data.objects) { 104 | var obj = data.objects[name]; 105 | if (!obj) continue; 106 | if (obj.location) 107 | obj.location = swapAxes(obj.location); 108 | if (obj.scale) 109 | obj.scale = swapAxes(obj.scale); 110 | } 111 | } 112 | return data; 113 | } 114 | 115 | function swapAxesScene(scene) { 116 | if (scene.gravity) 117 | scene.gravity = swapAxes(scene.gravity); 118 | return scene; 119 | } 120 | 121 | websocket.onmessage = function(event) { 122 | try { 123 | var data = JSON.parse(event.data); 124 | } catch(err) { 125 | emit("badFormat", event.data); 126 | return; 127 | } 128 | 129 | switch (data[0]) { 130 | case "app": 131 | if (!self.connected) { 132 | self.connected = true; 133 | emit("open", data[1]); 134 | } 135 | break; 136 | 137 | case "context": 138 | self.context = data[1]; 139 | emit("context", self.context); 140 | break; 141 | 142 | case "data": 143 | var diff = swapAxesData(data[1]); 144 | for (var collection in diff) { 145 | if (!self.data.hasOwnProperty(collection)) 146 | self.data[collection] = {}; 147 | for (var name in diff[collection]) { 148 | var add = !self.data[collection][name]; 149 | self.data[collection][name] = diff[collection][name]; 150 | if (add) 151 | emit("add", collection, name); 152 | } 153 | for (var name in self.data[collection]) 154 | if (diff[collection][name] === null) { 155 | emit("remove", collection, name); 156 | delete self.data[collection][name]; 157 | } 158 | } 159 | emit("data", self.data, diff); 160 | break; 161 | 162 | case "scene": 163 | var name = data[1]; 164 | if (!data[2]) { 165 | emit("remove", "scenes", name); 166 | delete self.scenes[name]; 167 | } else { 168 | self.scenes[name] = swapAxesScene(data[2]); 169 | emit("add", "scenes", name); 170 | } 171 | emit("scene", name, self.scenes[name]); 172 | break; 173 | 174 | default: 175 | emit("unknownMessage", data); 176 | break; 177 | } 178 | }; 179 | }; 180 | 181 | BlenderWebSocket.prototype.removeListener = BlenderWebSocket.prototype.off = function(event, handler) { 182 | if (!this.listeners[event]) 183 | return; 184 | var index = this.listeners[event].indexOf(handler); 185 | if (index !== -1) 186 | this.listeners[event].splice(index, 1); 187 | }; 188 | 189 | BlenderWebSocket.prototype.setAxes = function(axes) { 190 | var lowerAxes = axes.toLowerCase(); 191 | var indexes = "xyz"; 192 | for (var i = 0; i < 3; ++i) { 193 | this.axes[i] = { 194 | index: (i < axes.length ? indexes.indexOf(lowerAxes[i]) : i), 195 | scale: (lowerAxes[i] === axes[i] ? 1 : -1) 196 | }; 197 | } 198 | }; 199 | 200 | BlenderWebSocket.prototype.setContext = function(context) { 201 | if (this.websocket) 202 | this.websocket.send(JSON.stringify(["context", context])); 203 | }; 204 | 205 | BlenderWebSocket.prototype.setData = function(data) { 206 | if (this.websocket) 207 | this.websocket.send(JSON.stringify(["data", data])); 208 | }; 209 | 210 | BlenderWebSocket.prototype.setScene = function(scene, diff) { 211 | if (this.websocket) 212 | this.websocket.send(JSON.stringify(["scene", scene, diff])); 213 | }; 214 | 215 | return BlenderWebSocket; 216 | })(); 217 | -------------------------------------------------------------------------------- /usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blender client 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 |

19 | 		

20 | 		

21 | 		
69 | 	
70 |  
71 | 
72 | 


--------------------------------------------------------------------------------
/websocket_server.py:
--------------------------------------------------------------------------------
  1 | # WebSocket server for Blender
  2 | # Version 0.1.0
  3 | # Copyright 2015 Jonathan Giroux (Bloutiouf)
  4 | # Licensed under MIT (http://opensource.org/licenses/MIT)
  5 | 
  6 | bl_info = {
  7 |     "name": "WebSocket server",
  8 |     "author": "Jonathan Giroux (Bloutiouf)",
  9 |     "version": (0, 1, 0),
 10 |     "blender": (2, 69, 0),
 11 |     "description": "Send Blender's state over a WebSocket connection.",
 12 |     "category": "Import-Export"
 13 | }
 14 | 
 15 | import bpy
 16 | from bpy.app.handlers import persistent
 17 | from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
 18 | from bpy.types import AddonPreferences, Operator, Panel, PropertyGroup, USERPREF_HT_header, WindowManager
 19 | 
 20 | import copy
 21 | import json
 22 | import mathutils
 23 | import queue
 24 | import threading
 25 | 
 26 | from wsgiref.simple_server import make_server
 27 | from ws4py.websocket import WebSocket as _WebSocket
 28 | from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
 29 | from ws4py.server.wsgiutils import WebSocketWSGIApplication
 30 | 
 31 | class JSONEncoder(json.JSONEncoder):
 32 |     def default(self, obj):
 33 |         # print(type(obj))
 34 |         
 35 |         if isinstance(obj, bpy.types.BlendData):
 36 |             return {
 37 |                 "objects": list(self.default(object) for object in obj.objects)
 38 |             }
 39 |             
 40 |         if isinstance(obj, bpy.types.Camera):
 41 |             return {
 42 |                 "angle": obj.angle
 43 |             }
 44 |             
 45 |         if isinstance(obj, bpy.types.Mesh):
 46 |             return None
 47 |             
 48 |         if isinstance(obj, bpy.types.Object):
 49 |             rotation = obj.rotation_euler
 50 |             if obj.rotation_mode == 'AXIS_ANGLE':
 51 |                 rotation = list(obj.rotation_axis_angle)
 52 |             elif obj.rotation_mode == 'QUATERNION':
 53 |                 rotation = obj.rotation_quaternion
 54 |             r = {
 55 |                 "location": self.default(obj.location),
 56 |                 "rotation": self.default(rotation),
 57 |                 "rotationMode": obj.rotation_mode,
 58 |                 "scale": self.default(obj.scale),
 59 |                 "type": obj.type
 60 |             }
 61 |             if obj.data:
 62 |                 r["data"] = obj.data.name
 63 |             return r
 64 |             
 65 |         if isinstance(obj, bpy.types.PointLamp):
 66 |             return {
 67 |                 "color": self.default(obj.color),
 68 |                 "power": obj.energy * 8 + 10,
 69 |                 "type": obj.type
 70 |             }
 71 |             
 72 |         if isinstance(obj, bpy.types.Scene):
 73 |             return {
 74 |                 "camera": obj.camera.name if obj.camera else None
 75 |             }
 76 |             
 77 |         if isinstance(obj, bpy.types.SpotLamp):
 78 |             return {
 79 |                 "angle": obj.spot_size / 2,
 80 |                 "blend": obj.spot_blend,
 81 |                 "color": self.default(obj.color),
 82 |                 "power": obj.energy * 8 + 10,
 83 |                 "type": obj.type
 84 |             }
 85 |             
 86 |         if isinstance(obj, bpy.types.SunLamp):
 87 |             return {
 88 |                 "color": self.default(obj.color),
 89 |                 "power": obj.energy * 8 + 10,
 90 |                 "type": obj.type
 91 |             }
 92 |             
 93 |         if isinstance(obj, bpy.types.World):
 94 |             r = {
 95 |                 "ambiantColor": self.default(obj.ambiant_color),
 96 |                 "ambientOcclusionBlendType": obj.light_settings.ao_blend_type,
 97 |                 "ambientOcclusionFactor": obj.light_settings.ao_factor,
 98 |                 "colorRange": obj.color_range,
 99 |                 "environmentColor": obj.light_settings.environment_color,
100 |                 "environmentEnergy": obj.light_settings.environment_energy,
101 |                 "exposure": obj.exposure,
102 |                 "falloffStrength": obj.light_settings.falloff_strength,
103 |                 "gatherMethod": obj.gather_method,
104 |                 "horizonColor": self.default(obj.horizon_color),
105 |                 "indirectBounces": obj.light_settings.environment_bounces,
106 |                 "indirectFactor": obj.light_settings.environment_factor,
107 |                 "useAmbientOcclusion": obj.light_settings.use_ambient_occlusion,
108 |                 "useEnvironmentLighting": obj.light_settings.use_environment_light,
109 |                 "useFalloff": obj.light_settings.use_falloff,
110 |                 "useIndirectLighting": obj.light_settings.use_indirect_light,
111 |                 "useMist": obj.mist_settings.use_mist,
112 |                 "useSkyBlend": obj.use_sky_blend,
113 |                 "useSkyPaper": obj.use_sky_paper,
114 |                 "useSkyReal": obj.use_sky_real,
115 |                 "zenithColor": self.default(obj.zenith_color),
116 |             }
117 |             if obj.gather_method == "RAYTRACE":
118 |                 r["samples"] = obj.light_settings.samples
119 |                 r["samplingMethod"] = obj.light_settings.sample_method
120 |                 r["distance"] = obj.light_settings.distance
121 |             if obj.gather_method == "APPROXIMATE":
122 |                 r["correction"] = obj.light_settings.correction
123 |                 r["errorThreshold"] = obj.light_settings.error_threshold
124 |                 r["passes"] = obj.light_settings.passes
125 |                 r["useCache"] = obj.light_settings.use_cache
126 |             if obj.mist_settings.use_mist:
127 |                 r["mist"] = 2
128 |             return r
129 |             
130 |         if isinstance(obj, bpy.types.TimelineMarker):
131 |             return {
132 |                 "frame": obj.frame,
133 |                 "name": obj.name
134 |             }
135 |             
136 |         if isinstance(obj, mathutils.Color):
137 |             return list(obj)
138 |             
139 |         if isinstance(obj, mathutils.Euler):
140 |             return list(obj)
141 |             
142 |         if isinstance(obj, mathutils.Quaternion):
143 |             return list(obj)
144 |             
145 |         if isinstance(obj, mathutils.Vector):
146 |             return list(obj)
147 |             
148 |         return json.JSONEncoder.default(self, obj)
149 | 
150 | def stringify(data):
151 |     return JSONEncoder(separators=(",", ":")).encode(data)
152 |     
153 | previous_context = {}
154 | previous_data_keys = {}
155 | previous_scenes = {}
156 | 
157 | def get_context(addon_prefs, diff):
158 |     global previous_context
159 |     
160 |     current_context = {
161 |         "filePath": bpy.data.filepath,
162 |         "selectedObjects": hasattr(bpy.context, "selected_objects") and list(object.name for object in bpy.context.selected_objects)
163 |     }
164 |     
165 |     if previous_context == current_context and diff:
166 |         return
167 |         
168 |     previous_context = current_context
169 |     return current_context
170 | 
171 | def get_data(addon_prefs, diff):
172 |     global previous_data_keys
173 |     
174 |     data = {}
175 |     
176 |     def fill(name, collection):
177 |         if collection.is_updated or not diff:
178 |             data[name] = {}
179 |             if name in previous_data_keys:
180 |                 for n in previous_data_keys[name]:
181 |                     if n not in collection:
182 |                         data[name][n] = None
183 |             for obj in collection:
184 |                 if obj.is_updated or not diff:
185 |                     data[name][obj.name] = obj
186 |             previous_data_keys[name] = collection.keys()
187 |             if len(data[name]) == 0 and diff:
188 |                 del data[name]
189 |     
190 |     if 'CAMERAS' in addon_prefs.data_to_send:
191 |         fill("cameras", bpy.data.cameras)
192 |     if 'LAMPS' in addon_prefs.data_to_send:
193 |         fill("lamps", bpy.data.lamps)
194 |     if 'OBJECTS' in addon_prefs.data_to_send:
195 |         fill("objects", bpy.data.objects)
196 |     if 'WORLDS' in addon_prefs.data_to_send:
197 |         fill("worlds", bpy.data.worlds)
198 |     
199 |     if len(data) == 0 and diff:
200 |         return
201 |         
202 |     return data
203 | 
204 | def get_scene(scene, addon_prefs, diff):
205 |     global previous_scenes
206 |     previous_scene = previous_scenes.get(scene.name, None)
207 |     
208 |     current_scene = {
209 |         "activeObject": scene.objects.active and scene.objects.active.name,
210 |         "camera": scene.camera and scene.camera.name,
211 |         "fps": scene.render.fps / scene.render.fps_base,
212 |         "frame": scene.frame_current,
213 |         "frameEnd": scene.frame_end,
214 |         "frameStart": scene.frame_start,
215 |         "gravity": scene.gravity,
216 |         "objects": list(object.name for object in scene.objects),
217 |         "timelineMarkers": list(scene.timeline_markers),
218 |         "world": scene.world and scene.world.name
219 |     }
220 |     
221 |     if previous_scene == current_scene and diff:
222 |         return
223 |         
224 |     previous_scenes[scene.name] = current_scene
225 |     return current_scene
226 | 
227 | def broadcast(sockets, message):
228 |     for socket in sockets:
229 |         socket.send(message)
230 | 
231 | def send_state(sockets):
232 |     addon_prefs = bpy.context.user_preferences.addons[__name__].preferences
233 |     
234 |     broadcast(sockets, stringify(("app", {
235 |         "version": bpy.app.version,
236 |         "versionString": bpy.app.version_string
237 |     })))
238 |     
239 |     data = get_data(addon_prefs, False)
240 |     if data:
241 |         broadcast(sockets, stringify(("data", data)))
242 |     
243 |     if 'SCENES' in addon_prefs.data_to_send:
244 |         for scene in bpy.data.scenes:
245 |             data = get_scene(scene, addon_prefs, False)
246 |             if data:
247 |                 broadcast(sockets, stringify(("scene", scene.name, data)))
248 |     
249 |     if 'CONTEXT' in addon_prefs.data_to_send:
250 |         data = get_context(addon_prefs, False)
251 |         if data:
252 |             broadcast(sockets, stringify(("context", data)))
253 | 
254 | message_queue = queue.Queue()
255 | sockets = []
256 | 
257 | class WebSocketApp(_WebSocket):
258 |     def opened(self):
259 |         send_state([self])
260 |         sockets.append(self)
261 |         
262 |     def closed(self, code, reason=None):
263 |         sockets.remove(self)
264 |         
265 |     def received_message(self, message):
266 |         data = json.loads(message.data.decode(message.encoding))
267 |         message_queue.put(data)
268 |     
269 | @persistent
270 | def load_post():
271 |     send_state(sockets)
272 | 
273 | @persistent
274 | def scene_update_post(scene):
275 |     addon_prefs = bpy.context.user_preferences.addons[__name__].preferences
276 |     
277 |     data = get_data(addon_prefs, True)
278 |     if data:
279 |         broadcast(sockets, stringify(("data", data)))
280 |     
281 |     if 'SCENES' in addon_prefs.data_to_send:
282 |         scene_diff = set(previous_scenes.keys()) - set(bpy.data.scenes.keys())
283 |         for scene in scene_diff:
284 |             broadcast(sockets, stringify(("scene", scene)))
285 |             del previous_scenes[scene]
286 |         
287 |         data = get_scene(scene, addon_prefs, True)
288 |         if data:
289 |             broadcast(sockets, stringify(("scene", scene.name, data)))
290 |     
291 |     if 'CONTEXT' in addon_prefs.data_to_send:
292 |         data = get_context(addon_prefs, True)
293 |         if data:
294 |             broadcast(sockets, stringify(("context", data)))
295 |     
296 |     while not message_queue.empty():
297 |         data = message_queue.get()
298 |         if data[0] == "scene" and data[1] in bpy.data.scenes:
299 |             scene = bpy.data.scenes[data[1]]
300 |             diff = data[2]
301 |             if "frame" in diff:
302 |                 scene.frame_current = diff["frame"]
303 |               
304 | wserver = None
305 | 
306 | def start_server(host, port):
307 |     global wserver
308 |     if wserver:
309 |         return False
310 |     
311 |     wserver = make_server(host, port,
312 |         server_class=WSGIServer,
313 |         handler_class=WebSocketWSGIRequestHandler,
314 |         app=WebSocketWSGIApplication(handler_cls=WebSocketApp)
315 |     )
316 |     wserver.initialize_websockets_manager()
317 |     
318 |     wserver_thread = threading.Thread(target=wserver.serve_forever)
319 |     wserver_thread.daemon = True
320 |     wserver_thread.start()
321 |     
322 |     bpy.app.handlers.load_post.append(load_post)
323 |     bpy.app.handlers.scene_update_post.append(scene_update_post)
324 |     
325 |     return True
326 | 
327 | def stop_server():
328 |     global wserver
329 |     if not wserver:
330 |         return False
331 |         
332 |     wserver.shutdown()
333 |     for socket in sockets:
334 |         socket.close()
335 |         
336 |     wserver = None
337 |     
338 |     bpy.app.handlers.load_post.remove(load_post)
339 |     bpy.app.handlers.scene_update_post.remove(scene_update_post)
340 |     
341 |     return True
342 | 
343 | 
344 | class WebSocketServerSettings(AddonPreferences):
345 |     bl_idname = __name__
346 |     
347 |     auto_start = BoolProperty(
348 |         name="Start automatically",
349 |         description="Automatically start the server when loading the add-on",
350 |         default=True
351 |     )
352 |     
353 |     host = StringProperty(
354 |         name="Host",
355 |         description="Listen on host:port",
356 |         default="localhost"
357 |     )
358 |     
359 |     port = IntProperty(
360 |         name="Port",
361 |         description="Listen on host:port",
362 |         default=8137,
363 |         min=0,
364 |         max=65535,
365 |         subtype="UNSIGNED"
366 |     )
367 |     
368 |     data_to_send = EnumProperty(
369 |         items=[
370 |             ('ACTIONS', "Actions", "Action data"),
371 |             ('ARMATURES', "Armatures", "Armature data"),
372 |             ('BRUSHES', "Brushes", "Brush data"),
373 |             ('CAMERAS', "Cameras", "Camera data"),
374 |             ('CONTEXT', "Context", "Context data"),
375 |             ('CURVES', "Curves", "Curve data"),
376 |             ('FONTS', "Fonts", "Font data"),
377 |             ('GREASE_PENCILS', "Grease pencils", "Grease pencil data"),
378 |             ('IMAGES', "Images", "Image data"),
379 |             ('LAMPS', "Lamps", "Lamp data"),
380 |             ('MASKS', "Masks", "Mask data"),
381 |             ('MATERIALS', "Materials", "Material data"),
382 |             ('MESHES', "Meshes", "Mesh data"),
383 |             ('METABALLS', "Metaballs", "Metaball data"),
384 |             ('MOVIECLIPS', "Movieclips", "Movieclip data"),
385 |             ('NODE_TREES', "Node trees", "Node tree data"),
386 |             ('OBJECTS', "Objects", "Object data"),
387 |             ('PARTICLES', "Particles", "Particle data"),
388 |             ('SCENES', "Scenes", "Scene data"),
389 |             ('SCREENS', "Screens", "Screen data"),
390 |             ('SHAPE_KEYS', "Shape keys", "Shape key data"),
391 |             ('SOUNDS', "Sounds", "Sound data"),
392 |             ('SPEAKERS', "Speakers", "Speaker data"),
393 |             ('TEXTURES', "Textures", "Texture data"),
394 |             ('WORLDS', "Worlds", "World data")
395 |         ],
396 |         name="Data to send",
397 |         description="Specify which data are sent to the clients",
398 |         default={'OBJECTS', 'SCENES'},
399 |         options={'ENUM_FLAG'}
400 |     )
401 |         
402 |     def draw(self, context):
403 |         layout = self.layout
404 |         
405 |         row = layout.row()
406 |         split = row.split(percentage=0.3)
407 |         
408 |         col = split.column()
409 |         col.prop(self, "host")
410 |         col.prop(self, "port")
411 |         col.separator()
412 |         
413 |         col.prop(self, "auto_start")
414 |         
415 |         if wserver:
416 |             col.operator(Stop.bl_idname, icon='QUIT', text="Stop server")
417 |         else:
418 |             col.operator(Start.bl_idname, icon='QUIT', text="Start server")
419 |             
420 |         col = split.column()
421 |         col.label("Data to send:", icon='RECOVER_LAST')
422 |         col.prop(self, "data_to_send", expand=True)
423 | 
424 | class Start(Operator):
425 |     """Start WebSocket server"""
426 |     bl_idname = "websocket_server.start"
427 |     bl_label = "Start WebSocket server"
428 |     
429 |     def execute(self, context):
430 |         addon_prefs = context.user_preferences.addons[__name__].preferences
431 |         if not start_server(str(addon_prefs.host), int(addon_prefs.port)):
432 |             self.report({"ERROR"}, "The server is already started.")
433 |             return {"CANCELLED"}
434 |         return {"FINISHED"}
435 | 
436 | class Stop(Operator):
437 |     """Stop WebSocket server"""
438 |     bl_idname = "websocket_server.stop"
439 |     bl_label = "Stop WebSocket server"
440 |     
441 |     def execute(self, context):
442 |         if not stop_server():
443 |             self.report({"ERROR"}, "The server is not started.")
444 |             return {"CANCELLED"}
445 |         return {"FINISHED"}
446 |     
447 | def register():
448 |     bpy.utils.register_module(__name__)
449 |     
450 |     addon_prefs = bpy.context.user_preferences.addons[__name__].preferences
451 |     if bool(addon_prefs.auto_start):
452 |         start_server(str(addon_prefs.host), int(addon_prefs.port))
453 | 
454 | def unregister():
455 |     stop_server()
456 |     bpy.utils.unregister_module(__name__)
457 |     
458 | if __name__ == "__main__":
459 |     register()
460 | 


--------------------------------------------------------------------------------