From 1789a432a152f365b6098de512be53234cb2d68e Mon Sep 17 00:00:00 2001 From: Swann Martinez Date: Wed, 10 Apr 2019 17:01:21 +0200 Subject: [PATCH] refactor: cleanup --- __init__.py | 17 +- net_components.py => client.py | 219 +++++----------- net_draw.py => draw.py | 0 helpers.py | 274 ++++++++++++++++++++ libs/bsyncio | 1 + libs/esper | 1 + message.py | 84 +++++++ net_operators.py => operators.py | 412 ++++++------------------------- server.py | 87 ++++++- test_client.py | 2 +- net_ui.py => ui.py | 46 ++-- 11 files changed, 616 insertions(+), 527 deletions(-) rename net_components.py => client.py (52%) rename net_draw.py => draw.py (100%) create mode 100644 helpers.py create mode 160000 libs/bsyncio create mode 160000 libs/esper create mode 100644 message.py rename net_operators.py => operators.py (61%) rename net_ui.py => ui.py (82%) diff --git a/__init__.py b/__init__.py index 874306d..a86fbb1 100644 --- a/__init__.py +++ b/__init__.py @@ -14,17 +14,16 @@ import os import sys -from . import net_operators -from . import net_ui -from . import net_ecs +from . import operators +from . import ui def register(): - net_operators.register() - net_ui.register() - net_ecs.register() + operators.register() + ui.register() + def unregister(): - net_ui.unregister() - net_operators.unregister() - net_ecs.unregister() + ui.unregister() + operators.unregister() + \ No newline at end of file diff --git a/net_components.py b/client.py similarity index 52% rename from net_components.py rename to client.py index 9d730f7..656b9ad 100644 --- a/net_components.py +++ b/client.py @@ -1,21 +1,29 @@ +import binascii import collections import logging -import threading -from uuid import uuid4 -import binascii import os import sys -from random import randint +import threading import time from enum import Enum +from random import randint +from uuid import uuid4 + + try: from .libs import umsgpack from .libs import zmq + from .libs import dump_anything + from . import helpers + from . import message except: # Server import from libs import umsgpack from libs import zmq + from libs import dump_anything + import helpers + import message logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) @@ -26,6 +34,7 @@ SERVER_MAX = 1 stop = False + def zpipe(ctx): """build inproc pipe for talking to threads @@ -48,75 +57,7 @@ class State(Enum): SYNCING = 2 ACTIVE = 3 -class RCFMessage(object): - """ - Message is formatted on wire as 2 frames: - frame 0: key (0MQ string) // property path - frame 1: id (0MQ string) // property path - frame 2: mtype (0MQ string) // property path - frame 3: body (blob) // Could be any data - """ - key = None # key (string) - id = None # User (string) - mtype = None # data mtype (string) - body = None # data blob - uuid = None - - def __init__(self, key=None, uuid=None, id=None, mtype=None, body=None): - if uuid is None: - uuid = uuid4().bytes - - self.key = key - self.uuid = uuid - self.mtype = mtype - self.body = body - self.id = id - - def store(self, dikt): - """Store me in a dict if I have anything to store""" - # this currently erasing old value - if self.key is not None: - dikt[self.key] = self - # elif self.key in dikt: - # del dikt[self.key] - - def send(self, socket): - """Send key-value message to socket; any empty frames are sent as such.""" - key = ''.encode() if self.key is None else self.key.encode() - mtype = ''.encode() if self.mtype is None else self.mtype.encode() - body = ''.encode() if self.body is None else umsgpack.packb(self.body) - id = ''.encode() if self.id is None else self.id - - try: - socket.send_multipart([key, id, mtype, body]) - except: - logger.info("Fail to send {} {}".format(key,id)) - - @classmethod - def recv(cls, socket): - """Reads key-value message from socket, returns new kvmsg instance.""" - key, id, mtype, body = socket.recv_multipart(zmq.DONTWAIT) - key = key.decode() if key else None - id = id if id else None - mtype = mtype.decode() if body else None - body = umsgpack.unpackb(body) if body else None - - return cls(key=key, id=id, mtype=mtype, body=body) - - def dump(self): - if self.body is None: - size = 0 - data = 'NULL' - else: - size = len(self.body) - data = repr(self.body) - print("[key:{key}][size:{size}][mtype:{mtype}] {data}".format( - key=self.key, - size=size, - mtype=self.mtype, - data=data, - )) class RCFClient(object): ctx = None @@ -139,7 +80,8 @@ class RCFClient(object): """Set new value in distributed hash table Sends [SET][key][value] to the agent """ - self.pipe.send_multipart([b"SET", umsgpack.packb(key), umsgpack.packb(value)]) + self.pipe.send_multipart( + [b"SET", umsgpack.packb(key), umsgpack.packb(value)]) def get(self, key): """Lookup value in distributed hash table @@ -160,13 +102,14 @@ class RCFClient(object): global stop stop = True + class RCFServer(object): address = None # Server address port = None # Server port snapshot = None # Snapshot socket subscriber = None # Incoming updates - def __init__(self, ctx, address, port,id): + def __init__(self, ctx, address, port, id): self.address = address self.port = port self.snapshot = ctx.socket(zmq.DEALER) @@ -179,6 +122,7 @@ class RCFServer(object): self.subscriber.linger = 0 print("connected on tcp://{}:{}".format(address.decode(), port)) + class RCFClientAgent(object): ctx = None pipe = None @@ -210,30 +154,33 @@ class RCFClientAgent(object): if self.server is None: self.server = RCFServer(self.ctx, address, port, self.id) - self.publisher.connect("tcp://{}:{}".format(address.decode(), port+2)) + self.publisher.connect( + "tcp://{}:{}".format(address.decode(), port+2)) else: logger.error("E: too many servers (max. %i)", SERVER_MAX) - + elif command == b"SET": - key,value = msg + key, value = msg # Send key-value pair on to server - rcfmsg = RCFMessage(key=umsgpack.unpackb(key),id=self.id ,mtype="",body=umsgpack.unpackb(value)) + rcfmsg = message.RCFMessage(key=umsgpack.unpackb( + key), id=self.id, mtype="", body=umsgpack.unpackb(value)) rcfmsg.store(self.property_map) - + rcfmsg.send(self.publisher) - + elif command == b"GET": key = umsgpack.unpackb(msg[0]) value = self.property_map.get(key) self.pipe.send(umsgpack.packb(value.body) if value else b'') + def rcf_client_agent(ctx, pipe): agent = RCFClientAgent(ctx, pipe) server = None global stop - while True: + while True: if stop: break # logger.info("asdasd") @@ -241,7 +188,6 @@ def rcf_client_agent(ctx, pipe): poller.register(agent.pipe, zmq.POLLIN) server_socket = None - if agent.state == State.INITIAL: server = agent.server if agent.server: @@ -261,12 +207,13 @@ def rcf_client_agent(ctx, pipe): try: items = dict(poller.poll(1)) except: - pass + raise + break if agent.pipe in items: agent.control_message() elif server_socket in items: - rcfmsg = RCFMessage.recv(server_socket) + rcfmsg = message.RCFMessage.recv(server_socket) if agent.state == State.SYNCING: # Store snapshot if rcfmsg.key == "SNAPSHOT_END": @@ -278,90 +225,58 @@ def rcf_client_agent(ctx, pipe): if rcfmsg.id != agent.id: rcfmsg.store(agent.property_map) action = "update" if rcfmsg.body else "delete" - logging.info("I: received from {}:{},{} {}".format(server.address,rcfmsg.body.id, server.port, action)) + logging.info("I: received from {}:{},{} {}".format( + server.address, rcfmsg.body.id, server.port, action)) else: logger.info("IDLE") logger.info("exit thread") stop = False - # else: else - # agent.state = State.INITIAL + # else: else + # agent.state = State.INITIAL -class RCFServerAgent(): - def __init__(self, context=zmq.Context.instance(), id="admin"): - self.context = context - self.pub_sock = None - self.request_sock = None - self.collector_sock = None - self.poller = None +class SerializationAgent(object): + ctx = None + pipe = None - self.property_map = {} - self.id = id - self.bind_ports() - # Main client loop registration - self.tick() + def __init__(self, ctx, pipe_in, pipe_out): + self.ctx = ctx + self.pipe_in = pipe_in + self.pipe_out = pipe_out - logger.info("{} client initialized".format(id)) + def control_message(self): + msg = self.pipe_in.recv_multipart() + command = msg.pop(0) - def bind_ports(self): - # Update all clients - self.pub_sock = self.context.socket(zmq.PUB) - self.pub_sock.setsockopt(zmq.SNDHWM, 60) - self.pub_sock.bind("tcp://*:5556") - time.sleep(0.2) + if command == b"DUMP": + key = umsgpack.unpackb(msg[0]) - # Update request - self.request_sock = self.context.socket(zmq.ROUTER) - self.request_sock.setsockopt(zmq.IDENTITY, b'SERVER') - self.request_sock.setsockopt(zmq.RCVHWM, 60) - self.request_sock.bind("tcp://*:5555") + logger.log("Dumping....") - # Update collector - self.collector_sock = self.context.socket(zmq.PULL) - self.collector_sock.setsockopt(zmq.RCVHWM, 60) - self.collector_sock.bind("tcp://*:5557") + elif command == b"LOAD": + key, value = msg + logger.log("Loading....") - # poller for socket aggregation - self.poller = zmq.Poller() - self.poller.register(self.request_sock, zmq.POLLIN) - self.poller.register(self.collector_sock, zmq.POLLIN) - def tick(self): - logger.info("{} server launched".format(id)) +def serialization_agent(ctx, pipe_in, pipe_out): + agent = SerializationAgent(ctx, pipe_in, pipe_out) + server = None - while True: - # Non blocking poller - socks = dict(self.poller.poll(1000)) + global stop + while True: + if stop: + break - # Snapshot system for late join (Server - Client) - if self.request_sock in socks: - msg = self.request_sock.recv_multipart(zmq.DONTWAIT) + poller = zmq.Poller() + poller.register(agent.pipe, zmq.POLLIN) - identity = msg[0] - request = msg[1] - print("asdasd") - if request == b"SNAPSHOT_REQUEST": - pass - else: - logger.info("Bad snapshot request") - break + try: + items = dict(poller.poll(1)) + except: + raise + break - for k, v in self.property_map.items(): - logger.info( - "Sending {} snapshot to {}".format(k, identity)) - self.request_sock.send(identity, zmq.SNDMORE) - v.send(self.request_sock) + if agent.pipe in items: + agent.control_message() - msg_end_snapshot = RCFMessage(key="SNAPSHOT_END", id=identity) - self.request_sock.send(identity, zmq.SNDMORE) - msg_end_snapshot.send(self.request_sock) - logger.info("done") - - # Regular update routing (Clients / Client) - elif self.collector_sock in socks: - msg = RCFMessage.recv(self.collector_sock) - # Update all clients - msg.store(self.property_map) - msg.send(self.pub_sock) - diff --git a/net_draw.py b/draw.py similarity index 100% rename from net_draw.py rename to draw.py diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..814d49d --- /dev/null +++ b/helpers.py @@ -0,0 +1,274 @@ +import bpy +from .libs import dump_anything + +def dump_datablock(datablock, depth): + if datablock: + print("sending {}".format(datablock.name)) + + dumper = dump_anything.Dumper() + dumper.type_subset = dumper.match_subset_all + dumper.depth = depth + + datablock_type = datablock.bl_rna.name + key = "{}/{}".format(datablock_type, datablock.name) + data = dumper.dump(datablock) + + client.push_update(key, datablock_type, data) + + +def dump_datablock_attibute(datablock, attributes, depth=1): + if datablock: + dumper = dump_anything.Dumper() + dumper.type_subset = dumper.match_subset_all + dumper.depth = depth + + datablock_type = datablock.bl_rna.name + key = "{}/{}".format(datablock_type, datablock.name) + + data = {} + for attr in attributes: + try: + data[attr] = dumper.dump(getattr(datablock, attr)) + except: + pass + + client.push_update(key, datablock_type, data) + + +def upload_mesh(mesh): + if mesh.bl_rna.name == 'Mesh': + dump_datablock_attibute( + mesh, ['name', 'polygons', 'edges', 'vertices'], 6) + + +def upload_material(material): + if material.bl_rna.name == 'Material': + dump_datablock_attibute(material, ['name', 'node_tree'], 7) + + +def upload_gpencil(gpencil): + if gpencil.bl_rna.name == 'Grease Pencil': + dump_datablock_attibute(gpencil, ['name', 'layers','materials'], 9) + + +def load_mesh(target=None, data=None, create=False): + import bmesh + + # TODO: handle error + mesh_buffer = bmesh.new() + + for i in data["vertices"]: + mesh_buffer.verts.new(data["vertices"][i]["co"]) + + mesh_buffer.verts.ensure_lookup_table() + + for i in data["edges"]: + verts = mesh_buffer.verts + v1 = data["edges"][i]["vertices"][0] + v2 = data["edges"][i]["vertices"][1] + mesh_buffer.edges.new([verts[v1], verts[v2]]) + + for p in data["polygons"]: + verts = [] + for v in data["polygons"][p]["vertices"]: + verts.append(mesh_buffer.verts[v]) + + if len(verts) > 0: + mesh_buffer.faces.new(verts) + + if target is None and create: + target = bpy.data.meshes.new(data["name"]) + + mesh_buffer.to_mesh(target) + + # Load other meshes metadata + dump_anything.load(target, data) + + +def load_object(target=None, data=None, create=False): + try: + if target is None and create: + pointer = None + + # Object specific constructor... + if data["data"] in bpy.data.meshes.keys(): + pointer = bpy.data.meshes[data["data"]] + elif data["data"] in bpy.data.lights.keys(): + pointer = bpy.data.lights[data["data"]] + elif data["data"] in bpy.data.cameras.keys(): + pointer = bpy.data.cameras[data["data"]] + elif data["data"] in bpy.data.curves.keys(): + pointer = bpy.data.curves[data["data"]] + elif data["data"] in bpy.data.grease_pencils.keys(): + pointer = bpy.data.grease_pencils[data["data"]] + + target = bpy.data.objects.new(data["name"], pointer) + + # Load other meshes metadata + dump_anything.load(target, data) + import mathutils + target.matrix_world = mathutils.Matrix(data["matrix_world"]) + + except: + print("Object {} loading error ".format(data["name"])) + + +def load_collection(target=None, data=None, create=False): + try: + if target is None and create: + target = bpy.data.collections.new(data["name"]) + + # Load other meshes metadata + # dump_anything.load(target, data) + + # load objects into collection + for object in data["objects"]: + target.objects.link(bpy.data.objects[object]) + + for object in target.objects.keys(): + if object not in data["objects"]: + target.objects.unlink(bpy.data.objects[object]) + except: + print("Collection loading error") + + +def load_scene(target=None, data=None, create=False): + try: + if target is None and create: + target = bpy.data.scenes.new(data["name"]) + + # Load other meshes metadata + dump_anything.load(target, data) + + # Load master collection + for object in data["collection"]["objects"]: + if object not in target.collection.objects.keys(): + target.collection.objects.link(bpy.data.objects[object]) + + for object in target.collection.objects.keys(): + if object not in data["collection"]["objects"]: + target.collection.objects.unlink(bpy.data.objects[object]) + # load collections + # TODO: Recursive link + for collection in data["collection"]["children"]: + if collection not in target.collection.children.keys(): + target.collection.children.link( + bpy.data.collections[collection]) + + # Load annotation + if data["grease_pencil"]: + target.grease_pencil = bpy.data.grease_pencils[data["grease_pencil"]["name"]] + except: + print("Scene loading error") + + +def load_material(target=None, data=None, create=False): + try: + if target is None and create: + target = bpy.data.materials.new(data["name"]) + + # Load other meshes metadata + dump_anything.load(target, data) + + # load nodes + for node in data["node_tree"]["nodes"]: + index = target.node_tree.nodes.find(node) + + if index is -1: + node_type = data["node_tree"]["nodes"][node]["bl_idname"] + + target.node_tree.nodes.new(type=node_type) + + dump_anything.load( + target.node_tree.nodes[index], data["node_tree"]["nodes"][node]) + + for input in data["node_tree"]["nodes"][node]["inputs"]: + + try: + target.node_tree.nodes[index].inputs[input].default_value = data[ + "node_tree"]["nodes"][node]["inputs"][input]["default_value"] + except: + pass + + # Load nodes links + target.node_tree.links.clear() + + for link in data["node_tree"]["links"]: + current_link = data["node_tree"]["links"][link] + input_socket = target.node_tree.nodes[current_link['to_node'] + ['name']].inputs[current_link['to_socket']['name']] + output_socket = target.node_tree.nodes[current_link['from_node'] + ['name']].outputs[current_link['from_socket']['name']] + + target.node_tree.links.new(input_socket, output_socket) + + except: + print("Material loading error") + + +def load_gpencil_layer(target=None,data=None, create=False): + + dump_anything.load(target, data) + + + for frame in data["frames"]: + try: + tframe = target.frames[frame] + except: + tframe = target.frames.new(frame) + dump_anything.load(tframe, data["frames"][frame]) + for stroke in data["frames"][frame]["strokes"]: + try: + tstroke = tframe.strokes[stroke] + except: + tstroke = tframe.strokes.new() + dump_anything.load(tstroke, data["frames"][frame]["strokes"][stroke]) + + for point in data["frames"][frame]["strokes"][stroke]["points"]: + p = data["frames"][frame]["strokes"][stroke]["points"][point] + try: + tpoint = tstroke.points[point] + except: + tpoint = tstroke.points.add(1) + tpoint = tstroke.points[len(tstroke.points)-1] + dump_anything.load(tpoint, p) + + +def load_gpencil(target=None, data=None, create=False): + try: + if target is None and create: + target = bpy.data.grease_pencils.new(data["name"]) + + if "layers" in data.keys(): + for layer in data["layers"]: + if layer not in target.layers.keys(): + gp_layer = target.layers.new(data["layers"][layer]["info"]) + else: + gp_layer = target.layers[layer] + load_gpencil_layer(target=gp_layer,data=data["layers"][layer],create=create) + # Load other meshes metadata + dump_anything.load(target, data) + except: + print("default loading error") + + +def load_light(target=None, data=None, create=False, type=None): + try: + if target is None and create: + bpy.data.lights.new(data["name"], data["type"]) + + # Load other meshes metadata + dump_anything.load(target, data) + except: + print("light loading error") + + +def load_default(target=None, data=None, create=False, type=None): + try: + if target is None and create: + getattr(bpy.data, CORRESPONDANCE[type]).new(data["name"]) + + # Load other meshes metadata + dump_anything.load(target, data) + except: + print("default loading error") diff --git a/libs/bsyncio b/libs/bsyncio new file mode 160000 index 0000000..9527509 --- /dev/null +++ b/libs/bsyncio @@ -0,0 +1 @@ +Subproject commit 95275093b79d9289f939079550bb47ab25c1eacd diff --git a/libs/esper b/libs/esper new file mode 160000 index 0000000..5b6cd0c --- /dev/null +++ b/libs/esper @@ -0,0 +1 @@ +Subproject commit 5b6cd0c51718d5dcfa0e5613f824b5251cf092ac diff --git a/message.py b/message.py new file mode 100644 index 0000000..2289d8a --- /dev/null +++ b/message.py @@ -0,0 +1,84 @@ +from uuid import uuid4 + +try: + from .libs import umsgpack + from .libs import zmq +except: + # Server import + from libs import umsgpack + from libs import zmq + + +class RCFMessage(object): + """ + Message is formatted on wire as 2 frames: + frame 0: key (0MQ string) // property path + frame 1: id (0MQ string) // property path + frame 2: mtype (0MQ string) // property path + frame 3: body (blob) // Could be any data + + """ + key = None # key (string) + id = None # User (string) + mtype = None # data mtype (string) + body = None # data blob + uuid = None + + def __init__(self, key=None, uuid=None, id=None, mtype=None, body=None): + if uuid is None: + uuid = uuid4().bytes + + self.key = key + self.uuid = uuid + self.mtype = mtype + self.body = body + self.id = id + + def apply(self): + pass + + def store(self, dikt): + """Store me in a dict if I have anything to store""" + # this currently erasing old value + if self.key is not None: + dikt[self.key] = self + # elif self.key in dikt: + # del dikt[self.key] + + def send(self, socket): + """Send key-value message to socket; any empty frames are sent as such.""" + key = ''.encode() if self.key is None else self.key.encode() + mtype = ''.encode() if self.mtype is None else self.mtype.encode() + body = ''.encode() if self.body is None else umsgpack.packb(self.body) + id = ''.encode() if self.id is None else self.id + + try: + socket.send_multipart([key, id, mtype, body]) + except: + logger.info("Fail to send {} {}".format(key, id)) + + @classmethod + def recv(cls, socket): + """Reads key-value message from socket, returns new kvmsg instance.""" + key, id, mtype, body = socket.recv_multipart(zmq.DONTWAIT) + key = key.decode() if key else None + id = id if id else None + mtype = mtype.decode() if body else None + body = umsgpack.unpackb(body) if body else None + + return cls(key=key, id=id, mtype=mtype, body=body) + + def dump(self): + if self.body is None: + size = 0 + data = 'NULL' + else: + size = len(self.body) + data = repr(self.body) + print("[key:{key}][size:{size}][mtype:{mtype}] {data}".format( + key=self.key, + size=size, + mtype=self.mtype, + data=data, + )) + diff --git a/net_operators.py b/operators.py similarity index 61% rename from net_operators.py rename to operators.py index b70bb31..31f8e25 100644 --- a/net_operators.py +++ b/operators.py @@ -16,13 +16,12 @@ import mathutils from bpy_extras import view3d_utils from gpu_extras.batch import batch_for_shader -from . import net_components, net_ui, net_draw -from .libs import dump_anything +from . import client, ui, draw logger = logging.getLogger(__name__) -client = None +client_instance = None server = None context = None drawer = None @@ -99,101 +98,52 @@ def refresh_window(): import bpy bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) +def upload_client_instance_position(): + global client_instance -def dump_datablock(datablock, depth): - if datablock: - print("sending {}".format(datablock.name)) - - dumper = dump_anything.Dumper() - dumper.type_subset = dumper.match_subset_all - dumper.depth = depth - - datablock_type = datablock.bl_rna.name - key = "{}/{}".format(datablock_type, datablock.name) - data = dumper.dump(datablock) - - client.push_update(key, datablock_type, data) - - -def dump_datablock_attibute(datablock, attributes, depth=1): - if datablock: - dumper = dump_anything.Dumper() - dumper.type_subset = dumper.match_subset_all - dumper.depth = depth - - datablock_type = datablock.bl_rna.name - key = "{}/{}".format(datablock_type, datablock.name) - - data = {} - for attr in attributes: - try: - data[attr] = dumper.dump(getattr(datablock, attr)) - except: - pass - - client.push_update(key, datablock_type, data) - - -def upload_mesh(mesh): - if mesh.bl_rna.name == 'Mesh': - dump_datablock_attibute( - mesh, ['name', 'polygons', 'edges', 'vertices'], 6) - - -def upload_material(material): - if material.bl_rna.name == 'Material': - dump_datablock_attibute(material, ['name', 'node_tree'], 7) - -def upload_gpencil(gpencil): - if gpencil.bl_rna.name == 'Grease Pencil': - dump_datablock_attibute(gpencil, ['name', 'layers','materials'], 9) - -def upload_client_position(): - global client - - if client: - key = "net/clients/{}".format(client.id.decode()) + if client_instance: + key = "net/client_instances/{}".format(client_instance.id.decode()) try: - current_coords = net_draw.get_client_view_rect() - data = client.property_map[key].body + current_coords = net_draw.get_client_instance_view_rect() + data = client_instance.property_map[key].body if data is None: data = {} data['location'] = current_coords - color = bpy.context.scene.session_settings.client_color + color = bpy.context.scene.session_settings.client_instance_color data['color'] = (color.r, color.g, color.b, 1) - client.push_update(key, 'client', data) + client_instance.push_update(key, 'client_instance', data) elif current_coords[0] != data['location'][0]: data['location'] = current_coords - client.push_update(key, 'client', data) + client_instance.push_update(key, 'client_instance', data) except: pass def update_selected_object(context): - global client + global client_instance session = bpy.context.scene.session_settings # Active object bounding box if len(context.selected_objects) > 0: if session.active_object is not context.selected_objects[0] or session.active_object.is_evaluated: session.active_object = context.selected_objects[0] - key = "net/objects/{}".format(client.id.decode()) + key = "net/objects/{}".format(client_instance.id.decode()) data = {} - data['color'] = [session.client_color.r, - session.client_color.g, session.client_color.b] + data['color'] = [session.client_instance_color.r, + session.client_instance_color.g, session.client_instance_color.b] data['object'] = session.active_object.name - client.push_update( - key, 'clientObject', data) + client_instance.push_update( + key, 'client_instanceObject', data) return True elif len(context.selected_objects) == 0 and session.active_object: session.active_object = None data = {} - data['color'] = [session.client_color.r, - session.client_color.g, session.client_color.b] + data['color'] = [session.client_instance_color.r, + session.client_instance_color.g, session.client_instance_color.b] data['object'] = None - key = "net/objects/{}".format(client.id.decode()) - client.push_update(key, 'clientObject', data) + key = "net/objects/{}".format(client_instance.id.decode()) + client_instance.push_update(key, 'client_instanceObject', data) return True @@ -217,228 +167,8 @@ def init_scene(): for scene in bpy.data.scenes: dump_datablock(scene, 4) - -def load_mesh(target=None, data=None, create=False): - import bmesh - - # TODO: handle error - mesh_buffer = bmesh.new() - - for i in data["vertices"]: - mesh_buffer.verts.new(data["vertices"][i]["co"]) - - mesh_buffer.verts.ensure_lookup_table() - - for i in data["edges"]: - verts = mesh_buffer.verts - v1 = data["edges"][i]["vertices"][0] - v2 = data["edges"][i]["vertices"][1] - mesh_buffer.edges.new([verts[v1], verts[v2]]) - - for p in data["polygons"]: - verts = [] - for v in data["polygons"][p]["vertices"]: - verts.append(mesh_buffer.verts[v]) - - if len(verts) > 0: - mesh_buffer.faces.new(verts) - - if target is None and create: - target = bpy.data.meshes.new(data["name"]) - - mesh_buffer.to_mesh(target) - - # Load other meshes metadata - dump_anything.load(target, data) - - -def load_object(target=None, data=None, create=False): - try: - if target is None and create: - pointer = None - - # Object specific constructor... - if data["data"] in bpy.data.meshes.keys(): - pointer = bpy.data.meshes[data["data"]] - elif data["data"] in bpy.data.lights.keys(): - pointer = bpy.data.lights[data["data"]] - elif data["data"] in bpy.data.cameras.keys(): - pointer = bpy.data.cameras[data["data"]] - elif data["data"] in bpy.data.curves.keys(): - pointer = bpy.data.curves[data["data"]] - elif data["data"] in bpy.data.grease_pencils.keys(): - pointer = bpy.data.grease_pencils[data["data"]] - - target = bpy.data.objects.new(data["name"], pointer) - - # Load other meshes metadata - dump_anything.load(target, data) - import mathutils - target.matrix_world = mathutils.Matrix(data["matrix_world"]) - - except: - print("Object {} loading error ".format(data["name"])) - - -def load_collection(target=None, data=None, create=False): - try: - if target is None and create: - target = bpy.data.collections.new(data["name"]) - - # Load other meshes metadata - # dump_anything.load(target, data) - - # load objects into collection - for object in data["objects"]: - target.objects.link(bpy.data.objects[object]) - - for object in target.objects.keys(): - if object not in data["objects"]: - target.objects.unlink(bpy.data.objects[object]) - except: - print("Collection loading error") - - -def load_scene(target=None, data=None, create=False): - try: - if target is None and create: - target = bpy.data.scenes.new(data["name"]) - - # Load other meshes metadata - dump_anything.load(target, data) - - # Load master collection - for object in data["collection"]["objects"]: - if object not in target.collection.objects.keys(): - target.collection.objects.link(bpy.data.objects[object]) - - for object in target.collection.objects.keys(): - if object not in data["collection"]["objects"]: - target.collection.objects.unlink(bpy.data.objects[object]) - # load collections - # TODO: Recursive link - for collection in data["collection"]["children"]: - if collection not in target.collection.children.keys(): - target.collection.children.link( - bpy.data.collections[collection]) - - # Load annotation - if data["grease_pencil"]: - target.grease_pencil = bpy.data.grease_pencils[data["grease_pencil"]["name"]] - except: - print("Scene loading error") - - -def load_material(target=None, data=None, create=False): - try: - if target is None and create: - target = bpy.data.materials.new(data["name"]) - - # Load other meshes metadata - dump_anything.load(target, data) - - # load nodes - for node in data["node_tree"]["nodes"]: - index = target.node_tree.nodes.find(node) - - if index is -1: - node_type = data["node_tree"]["nodes"][node]["bl_idname"] - - target.node_tree.nodes.new(type=node_type) - - dump_anything.load( - target.node_tree.nodes[index], data["node_tree"]["nodes"][node]) - - for input in data["node_tree"]["nodes"][node]["inputs"]: - - try: - target.node_tree.nodes[index].inputs[input].default_value = data[ - "node_tree"]["nodes"][node]["inputs"][input]["default_value"] - except: - pass - - # Load nodes links - target.node_tree.links.clear() - - for link in data["node_tree"]["links"]: - current_link = data["node_tree"]["links"][link] - input_socket = target.node_tree.nodes[current_link['to_node'] - ['name']].inputs[current_link['to_socket']['name']] - output_socket = target.node_tree.nodes[current_link['from_node'] - ['name']].outputs[current_link['from_socket']['name']] - - target.node_tree.links.new(input_socket, output_socket) - - except: - print("Material loading error") - - -def load_gpencil_layer(target=None,data=None, create=False): - - dump_anything.load(target, data) - - - for frame in data["frames"]: - try: - tframe = target.frames[frame] - except: - tframe = target.frames.new(frame) - dump_anything.load(tframe, data["frames"][frame]) - for stroke in data["frames"][frame]["strokes"]: - try: - tstroke = tframe.strokes[stroke] - except: - tstroke = tframe.strokes.new() - dump_anything.load(tstroke, data["frames"][frame]["strokes"][stroke]) - - for point in data["frames"][frame]["strokes"][stroke]["points"]: - p = data["frames"][frame]["strokes"][stroke]["points"][point] - try: - tpoint = tstroke.points[point] - except: - tpoint = tstroke.points.add(1) - tpoint = tstroke.points[len(tstroke.points)-1] - dump_anything.load(tpoint, p) - -def load_gpencil(target=None, data=None, create=False): - try: - if target is None and create: - target = bpy.data.grease_pencils.new(data["name"]) - - if "layers" in data.keys(): - for layer in data["layers"]: - if layer not in target.layers.keys(): - gp_layer = target.layers.new(data["layers"][layer]["info"]) - else: - gp_layer = target.layers[layer] - load_gpencil_layer(target=gp_layer,data=data["layers"][layer],create=create) - # Load other meshes metadata - dump_anything.load(target, data) - except: - print("default loading error") - -def load_light(target=None, data=None, create=False, type=None): - try: - if target is None and create: - bpy.data.lights.new(data["name"], data["type"]) - - # Load other meshes metadata - dump_anything.load(target, data) - except: - print("light loading error") - -def load_default(target=None, data=None, create=False, type=None): - try: - if target is None and create: - getattr(bpy.data, CORRESPONDANCE[type]).new(data["name"]) - - # Load other meshes metadata - dump_anything.load(target, data) - except: - print("default loading error") - def update_scene(msg): - global client + global client_instance net_vars = bpy.context.scene.session_settings @@ -480,14 +210,14 @@ def update_scene(msg): # load_default(target=target, data=msg.body, # create=net_vars.load_data, type=msg.mtype) # else: - # if msg.mtype == 'client': + # if msg.mtype == 'client_instance': # refresh_window() - # elif msg.mtype == 'clientObject': + # elif msg.mtype == 'client_instanceObject': # selected_objects = [] - # for k, v in client.property_map.items(): - # if v.mtype == 'clientObject': - # if client.id != v.id: + # for k, v in client_instance.property_map.items(): + # if v.mtype == 'client_instanceObject': + # if client_instance.id != v.id: # selected_objects.append(v.body['object']) # for obj in bpy.data.objects: @@ -517,11 +247,11 @@ def push(data_type,id): dump_datablock(bpy.data.scenes[id], 4) def pull(keystore): - global client + global client_instance net_vars = bpy.context.scene.session_settings - body = client.property_map[keystore].body - data_type = client.property_map[keystore].mtype + body = client_instance.property_map[keystore].body + data_type = client_instance.property_map[keystore].mtype target = resolve_bpy_path(keystore) if target: @@ -553,14 +283,14 @@ def pull(keystore): elif data_type == 'Camera': load_default(target=target, data=body, create=net_vars.load_data, type=mtype) - elif data_type == 'client': + elif data_type == 'client_instance': refresh_window() - elif data_type == 'clientObject': + elif data_type == 'client_instanceObject': selected_objects = [] - for k, v in client.property_map.items(): - if v.mtype == 'clientObject': - if client.id != v.id: + for k, v in client_instance.property_map.items(): + if v.mtype == 'client_instanceObject': + if client_instance.id != v.id: selected_objects.append(v.body['object']) for obj in bpy.data.objects: @@ -613,10 +343,10 @@ def mesh_tick(): def object_tick(): obj_name = get_update("Object") - global client + global client_instance if obj_name: - if "Object/{}".format(obj_name) in client.property_map.keys(): + if "Object/{}".format(obj_name) in client_instance.property_map.keys(): dump_datablock_attibute(bpy.data.objects[obj_name], ['matrix_world']) else: dump_datablock(bpy.data.objects[obj_name], 1) @@ -635,7 +365,7 @@ def draw_tick(): drawer.draw() # Upload - upload_client_position() + upload_client_instance_position() return 0.2 @@ -669,7 +399,7 @@ class session_join(bpy.types.Operator): return True def execute(self, context): - global client, drawer + global client_instance, drawer net_settings = context.scene.session_settings # Scene setup @@ -684,13 +414,13 @@ class session_join(bpy.types.Operator): username = str(context.scene.session_settings.username) - client = net_components.RCFClient() - client.connect("127.0.0.1",5555) + client_instance = client.RCFClient() + client_instance.connect("127.0.0.1",5555) # net_settings.is_running = True - # drawer = net_draw.HUD(client_instance=client) + # drawer = net_draw.HUD(client_instance_instance=client_instance) # register_ticks() return {"FINISHED"} @@ -700,7 +430,7 @@ class session_join(bpy.types.Operator): class session_add_property(bpy.types.Operator): bl_idname = "session.add_prop" bl_label = "add" - bl_description = "broadcast a property to connected clients" + bl_description = "broadcast a property to connected client_instances" bl_options = {"REGISTER"} property_path: bpy.props.StringProperty(default="None") @@ -711,10 +441,10 @@ class session_add_property(bpy.types.Operator): return True def execute(self, context): - global client + global client_instance - client.set('key', 1) - print(client.get('key')) + client_instance.set('key', 1) + print(client_instance.get('key')) # item = resolve_bpy_path(self.property_path) # print(item) @@ -729,7 +459,7 @@ class session_add_property(bpy.types.Operator): # data = dumper.dump(item) # data_type = item.__class__.__name__ - # client.push_update(key, data_type, data) + # client_instance.push_update(key, data_type, data) return {"FINISHED"} @@ -737,7 +467,7 @@ class session_add_property(bpy.types.Operator): class session_remove_property(bpy.types.Operator): bl_idname = "session.remove_prop" bl_label = "remove" - bl_description = "broadcast a property to connected clients" + bl_description = "broadcast a property to connected client_instances" bl_options = {"REGISTER"} property_path: bpy.props.StringProperty(default="None") @@ -747,10 +477,10 @@ class session_remove_property(bpy.types.Operator): return True def execute(self, context): - global client + global client_instance try: - del client.property_map[self.property_path] + del client_instance.property_map[self.property_path] return {"FINISHED"} except: @@ -769,7 +499,7 @@ class session_create(bpy.types.Operator): def execute(self, context): global server - global client + global client_instance server = subprocess.Popen(['python','server.py'], shell=False, stdout=subprocess.PIPE) time.sleep(0.1) @@ -794,7 +524,7 @@ class session_stop(bpy.types.Operator): def execute(self, context): global server - global client + global client_instance net_settings = context.scene.session_settings @@ -802,16 +532,16 @@ class session_stop(bpy.types.Operator): server.kill() del server server = None - if client: - client.exit() - del client - client = None + if client_instance: + client_instance.exit() + del client_instance + client_instance = None # bpy.ops.asyncio.stop() net_settings.is_running = False # unregister_ticks() else: - logger.debug("No server/client running.") + logger.debug("No server/client_instance running.") return {"FINISHED"} @@ -840,30 +570,30 @@ class session_settings(bpy.types.PropertyGroup): ('HOST', 'hosting', 'host a session'), ('CONNECT', 'connexion', 'connect to a session')}, default='HOST') - client_color = bpy.props.FloatVectorProperty(name="client_color", + client_color = bpy.props.FloatVectorProperty(name="client_instance_color", subtype='COLOR', default=randomColor()) class session_snapview(bpy.types.Operator): bl_idname = "session.snapview" - bl_label = "draw clients" + bl_label = "draw client_instances" bl_description = "Description that shows in blender tooltips" bl_options = {"REGISTER"} - target_client = bpy.props.StringProperty() + target_client_instance = bpy.props.StringProperty() @classmethod def poll(cls, context): return True def execute(self, context): - global client + global client_instance area, region, rv3d = net_draw.view3d_find() - for k, v in client.property_map.items(): - if v.mtype == 'client' and v.id.decode() == self.target_client: + for k, v in client_instance.property_map.items(): + if v.mtype == 'client_instance' and v.id.decode() == self.target_client_instance: rv3d.view_location = v.body['location'][1] rv3d.view_distance = 30.0 return {"FINISHED"} @@ -892,9 +622,9 @@ def ordered(updates): def depsgraph_update(scene): - global client + global client_instance - if client and client.status == net_components.RCFStatus.CONNECTED: + if client_instance and client_instance.status == net_components.RCFStatus.CONNECTED: updates = bpy.context.depsgraph.updates update_selected_object(bpy.context) @@ -948,7 +678,7 @@ def depsgraph_update(scene): # if c[1].id.bl_rna.name == "Object": # if data_name in bpy.data.objects.keys(): # found = False - # for k in client.property_map.keys(): + # for k in client_instance.property_map.keys(): # if data_name in k: # found = True # break @@ -975,7 +705,7 @@ def register(): def unregister(): global server - global client + global client_instance # try: # bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update) @@ -986,10 +716,10 @@ def unregister(): # server.stop() del server server = None - if client: - # client.stop() - del client - client = None + if client_instance: + # client_instance.stop() + del client_instance + client_instance = None from bpy.utils import unregister_class for cls in reversed(classes): diff --git a/server.py b/server.py index 0aaee34..2f3479e 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,89 @@ -from net_components import RCFServerAgent +import logging +import time + +from libs import zmq + +import message +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +class RCFServerAgent(): + def __init__(self, context=zmq.Context.instance(), id="admin"): + self.context = context + + self.pub_sock = None + self.request_sock = None + self.collector_sock = None + self.poller = None + + self.property_map = {} + self.id = id + self.bind_ports() + # Main client loop registration + self.tick() + + logger.info("{} client initialized".format(id)) + + def bind_ports(self): + # Update all clients + self.pub_sock = self.context.socket(zmq.PUB) + self.pub_sock.setsockopt(zmq.SNDHWM, 60) + self.pub_sock.bind("tcp://*:5556") + time.sleep(0.2) + + # Update request + self.request_sock = self.context.socket(zmq.ROUTER) + self.request_sock.setsockopt(zmq.IDENTITY, b'SERVER') + self.request_sock.setsockopt(zmq.RCVHWM, 60) + self.request_sock.bind("tcp://*:5555") + + # Update collector + self.collector_sock = self.context.socket(zmq.PULL) + self.collector_sock.setsockopt(zmq.RCVHWM, 60) + self.collector_sock.bind("tcp://*:5557") + + # poller for socket aggregation + self.poller = zmq.Poller() + self.poller.register(self.request_sock, zmq.POLLIN) + self.poller.register(self.collector_sock, zmq.POLLIN) + + def tick(self): + logger.info("{} server launched".format(id)) + + while True: + # Non blocking poller + socks = dict(self.poller.poll(1000)) + + # Snapshot system for late join (Server - Client) + if self.request_sock in socks: + msg = self.request_sock.recv_multipart(zmq.DONTWAIT) + + identity = msg[0] + request = msg[1] + print("asdasd") + if request == b"SNAPSHOT_REQUEST": + pass + else: + logger.info("Bad snapshot request") + break + + for k, v in self.property_map.items(): + logger.info( + "Sending {} snapshot to {}".format(k, identity)) + self.request_sock.send(identity, zmq.SNDMORE) + v.send(self.request_sock) + + msg_end_snapshot = message.RCFMessage(key="SNAPSHOT_END", id=identity) + self.request_sock.send(identity, zmq.SNDMORE) + msg_end_snapshot.send(self.request_sock) + logger.info("done") + + # Regular update routing (Clients / Client) + elif self.collector_sock in socks: + msg = message.RCFMessage.recv(self.collector_sock) + # Update all clients + msg.store(self.property_map) + msg.send(self.pub_sock) server = RCFServerAgent() diff --git a/test_client.py b/test_client.py index b9ab3d1..5e8401e 100644 --- a/test_client.py +++ b/test_client.py @@ -1,4 +1,4 @@ -from net_components import RCFClient +from client import RCFClient import time client = RCFClient() diff --git a/net_ui.py b/ui.py similarity index 82% rename from net_ui.py rename to ui.py index 01fb372..8dbe35b 100644 --- a/net_ui.py +++ b/ui.py @@ -1,6 +1,6 @@ import bpy -from . import net_components, net_operators +from . import client, operators class SessionSettingsPanel(bpy.types.Panel): @@ -19,7 +19,7 @@ class SessionSettingsPanel(bpy.types.Panel): scene = context.scene row = layout.row() - if net_operators.client is None: + if operators.client_instance is None: row = layout.row() box = row.box() @@ -57,12 +57,12 @@ class SessionSettingsPanel(bpy.types.Panel): else: - if net_operators.client.agent.is_alive(): + if operators.client_instance.agent.is_alive(): row.label(text="Net frequency:") row.prop(net_settings, "update_frequency", text="") row = layout.row() row.operator("session.stop", icon='QUIT', text="Exit") - # elif net_operators.client.status is net_components.RCFStatus.CONNECTING: + # elif operators.client.status is client.RCFStatus.CONNECTING: # row.label(text="connecting...") # row = layout.row() # row.operator("session.stop", icon='QUIT', text="CANCEL") @@ -80,8 +80,8 @@ class SessionUsersPanel(bpy.types.Panel): @classmethod def poll(cls, context): - if net_operators.client: - return net_operators.client.status == net_components.RCFStatus.CONNECTED + if operators.client_instance: + return operators.client_instance.status == client.RCFStatus.CONNECTED return False def draw(self, context): @@ -91,21 +91,21 @@ class SessionUsersPanel(bpy.types.Panel): scene = context.scene # Create a simple row. row = layout.row() - if net_operators.client: - if len(net_operators.client.property_map) > 0: - for key, values in net_operators.client.property_map.items(): + if operators.client_instance: + if len(operators.client_instance.property_map) > 0: + for key, values in operators.client_instance.property_map.items(): if 'client' in key: info = "" item_box = row.box() detail_item_box = item_box.row() - if values.id == net_operators.client.id: + if values.id == operators.client_instance.id: info = "(self)" # detail_item_box = item_box.row() detail_item_box.label( text="{} - {}".format(values.id.decode(), info)) - if net_operators.client.id.decode() not in key: + if operators.client.id.decode() not in key: detail_item_box.operator( "session.snapview", text="", icon='VIEW_CAMERA').target_client = values.id.decode() row = layout.row() @@ -125,9 +125,9 @@ class SessionPropertiesPanel(bpy.types.Panel): @classmethod def poll(cls, context): - if net_operators.client: - return net_operators.client.agent.is_alive() - # return net_operators.client.status == net_components.RCFStatus.CONNECTED + if operators.client_instance: + return operators.client_instance.agent.is_alive() + # return operators.client.status == client.RCFStatus.CONNECTED return False def draw(self, context): @@ -139,7 +139,7 @@ class SessionPropertiesPanel(bpy.types.Panel): # Create a simple row. row = layout.row() - if net_operators.client: + if operators.client: row = layout.row(align=True) row.prop(net_settings, "buffer", text="") row.prop(net_settings, "add_property_depth", text="") @@ -150,8 +150,8 @@ class SessionPropertiesPanel(bpy.types.Panel): row = layout.row() # Property area area_msg = row.box() - # if len(net_operators.client.property_map) > 0: - # for key, values in net_operators.client.property_map.items(): + # if len(operators.client.property_map) > 0: + # for key, values in operators.client.property_map.items(): # item_box = area_msg.box() # detail_item_box = item_box.row() # # detail_item_box = item_box.row() @@ -173,9 +173,9 @@ class SessionTaskPanel(bpy.types.Panel): @classmethod def poll(cls, context): - if net_operators.client: - return net_operators.client.agent.is_alive() - # return net_operators.client.status == net_components.RCFStatus.CONNECTED + if operators.client: + return operators.client.agent.is_alive() + # return operators.client.status == client.RCFStatus.CONNECTED return False def draw(self, context): @@ -183,11 +183,11 @@ class SessionTaskPanel(bpy.types.Panel): # Create a simple row. row = layout.row() - if net_operators.update_list: + if operators.update_list: # Property area area_msg = row.box() - if len(net_operators.update_list) > 0: - for key, values in net_operators.update_list.items(): + if len(operators.update_list) > 0: + for key, values in operators.update_list.items(): item_box = area_msg.box() detail_item_box = item_box.row() # detail_item_box = item_box.row()