Skip to content

Instantly share code, notes, and snippets.

@andybak
Last active March 24, 2023 10:10
Show Gist options
  • Save andybak/55d11f1b29079ced31e8868aac3bd9e9 to your computer and use it in GitHub Desktop.
Save andybak/55d11f1b29079ced31e8868aac3bd9e9 to your computer and use it in GitHub Desktop.
import json
bl_info = {
"name": "Open Brush Connector",
"blender": (3, 4, 0),
"category": "System",
}
import bpy
import cgi
import http.server
import io
import socketserver
import threading
import urllib.error
import urllib.parse
import urllib.request
PORT = 8080
httpd = None
server_thread = None
def get_grease_pencil(gpencil_obj_name='GPencil') -> bpy.types.GreasePencil:
"""
Return the grease-pencil object with the given name. Initialize one if not already present.
:param gpencil_obj_name: name/key of the grease pencil object in the scene
"""
# If not present already, create grease pencil object
if gpencil_obj_name not in bpy.context.scene.objects:
bpy.ops.object.gpencil_add(align='WORLD', location=(0, 0, 0))
# rename grease pencil
bpy.context.scene.objects[-1].name = gpencil_obj_name
# Get grease pencil object
gpencil = bpy.context.scene.objects[gpencil_obj_name]
return gpencil
def get_grease_pencil_layer(gpencil: bpy.types.GreasePencil, gpencil_layer_name='GP_Layer',
clear_layer=False) -> bpy.types.GPencilLayer:
"""
Return the grease-pencil layer with the given name. Create one if not already present.
:param gpencil: grease-pencil object for the layer data
:param gpencil_layer_name: name/key of the grease pencil layer
:param clear_layer: whether to clear all previous layer data
"""
# Get grease pencil layer or create one if none exists
if gpencil.data.layers and gpencil_layer_name in gpencil.data.layers:
gpencil_layer = gpencil.data.layers[gpencil_layer_name]
else:
gpencil_layer = gpencil.data.layers.new(gpencil_layer_name, set_active=True)
if clear_layer:
gpencil_layer.clear() # clear all previous layer data
# bpy.ops.gpencil.paintmode_toggle() # need to trigger otherwise there is no frame
return gpencil_layer
# Util for default behavior merging previous two methods
def init_grease_pencil(gpencil_obj_name='GPencil', gpencil_layer_name='GP_Layer',
clear_layer=True) -> bpy.types.GPencilLayer:
gpencil = get_grease_pencil(gpencil_obj_name)
gpencil_layer = get_grease_pencil_layer(gpencil, gpencil_layer_name, clear_layer=clear_layer)
return gpencil_layer
class RequestHandler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data_stream = io.BytesIO(post_data)
form = cgi.FieldStorage(
fp=data_stream,
headers={'Content-Type': 'multipart/form-data'},
environ={'REQUEST_METHOD': 'POST'},
keep_blank_values=True
)
form_data = []
for key in form:
form_data.append((key, form[key].value))
self.handle_stroke_data(form_data)
def handle_stroke_data(self, data):
layer = init_grease_pencil()
frame = layer.frames.new(0)
for item in data:
cmd = item[0]
if cmd == 'draw.stroke':
json_string = f'{{"points": [{item[1]}]}}'
json_data = json.loads(json_string)
self.draw_stroke(frame, json_data['points'])
def draw_stroke(self, frame, points):
# Create a new stroke
stroke = frame.strokes.new()
stroke.display_mode = '3DSPACE' # Set stroke display mode to 3D
stroke.points.add(count=len(points)) # Add points to the stroke
# Assign coordinates to the stroke
for i, point in enumerate(points):
stroke.points[i].co = [point[0], point[2], point[1]]
def return_success_response(self):
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write("OK").encode('utf-8')
def start_http_server():
global httpd, server_thread
if httpd is None:
handler = RequestHandler
httpd = socketserver.TCPServer(("", PORT), handler)
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.start()
def stop_http_server():
global httpd, server_thread
if httpd is not None:
httpd.shutdown()
httpd.server_close()
server_thread.join()
httpd = None
server_thread = None
class HTTP_LISTENER_OT_start(bpy.types.Operator):
bl_idname = "http_listener.start"
bl_label = "Connect to Open Brush"
def execute(self, context):
setup_two_way_communication()
self.report({'INFO'}, "HTTP Listener started on port {}".format(PORT))
return {'FINISHED'}
class HTTP_LISTENER_OT_stop(bpy.types.Operator):
bl_idname = "http_listener.stop"
bl_label = "Disconnect from Open Brush"
def execute(self, context):
stop_http_server()
self.report({'INFO'}, "HTTP Listener stopped")
return {'FINISHED'}
class HTTP_LISTENER_PT_panel(bpy.types.Panel):
bl_label = "Open Brush Connection"
bl_idname = "HTTP_LISTENER_PT_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tool'
def draw(self, context):
layout = self.layout
row = layout.row()
row.operator("http_listener.start")
row = layout.row()
row.operator("http_listener.stop")
def register_listener():
OpenBrushBaseUrl = "http://localhost:40074/api/v1"
OpenBrushRegisterCommand = "listenfor.strokes"
ReceiverDomain = "localhost"
ReceiverBasePath = "/"
registerUrl = f"{OpenBrushBaseUrl}?{OpenBrushRegisterCommand}=http://{ReceiverDomain}:{PORT}{ReceiverBasePath}"
with urllib.request.urlopen(registerUrl) as response:
response_content = response.read()
def setup_two_way_communication():
start_http_server()
register_listener()
def register():
bpy.utils.register_class(HTTP_LISTENER_OT_start)
bpy.utils.register_class(HTTP_LISTENER_OT_stop)
bpy.utils.register_class(HTTP_LISTENER_PT_panel)
setup_two_way_communication()
def unregister():
bpy.utils.unregister_class(HTTP_LISTENER_OT_start)
bpy.utils.unregister_class(HTTP_LISTENER_OT_stop)
bpy.utils.unregister_class(HTTP_LISTENER_PT_panel)
stop_http_server()
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment