Skip to content

Instantly share code, notes, and snippets.

@unvestigate
Created April 17, 2025 08:12
Show Gist options
  • Select an option

  • Save unvestigate/92d99a62e3f53e751385b107e3c63455 to your computer and use it in GitHub Desktop.

Select an option

Save unvestigate/92d99a62e3f53e751385b107e3c63455 to your computer and use it in GitHub Desktop.

Revisions

  1. unvestigate created this gist Apr 17, 2025.
    541 changes: 541 additions & 0 deletions BlenderSyncLib.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,541 @@
    #include "BlenderSyncLibExport.h"

    #include <enet/enet.h>

    #include <stdio.h>
    #include <stdint.h>
    #include <string>
    #include <fstream>

    #include "../Basis/include/editor/BasisBlenderSync.h"

    #define BLENDERSYNC_MAX_PATH_LENGTH 2048
    #define BLENDERSYNC_RESPONSE_BUFFER_SIZE 2 * 1024

    // These are the return values of the service() function:
    #define BLENDERSYNC_EVENT_NONE 0
    #define BLENDERSYNC_EVENT_CLIENT_CONNECTED 1
    #define BLENDERSYNC_EVENT_CLIENT_DISCONNECTED 2
    #define BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED 3
    #define BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED 4

    // Longer timeouts:
    #define BLENDERSYNC_PEER_TIMEOUT_LIMIT 32
    #define BLENDERSYNC_PEER_TIMEOUT_MINIMUM 30000
    #define BLENDERSYNC_PEER_TIMEOUT_MAXIMUM 120000

    // The filesystem-based communication (which admittedly is a bit of a hack)
    // can be completely left out by commenting out this define.
    //#define BLENDERSYNC_FILESYSTEM_COMMS_ENABLED

    // Global state:

    ENetHost* gEnetHost = nullptr;
    ENetPeer* gEnetClientPeer = nullptr;

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    bool gRunningWithFilesystemComms = false;
    #endif

    uint32_t gEditID = 0xFFFFFFFF;

    float gTargetObjectRotationW = 1.0f;
    float gTargetObjectRotationX = 0.0f;
    float gTargetObjectRotationY = 0.0f;
    float gTargetObjectRotationZ = 0.0f;

    char gTargetMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1];
    char gSurroundingsMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1];

    uint8_t gTempBuffer[BLENDERSYNC_RESPONSE_BUFFER_SIZE];

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    // Returns the path to the filesystem-based communication data file.
    std::string getFSCommDataFilePath(bool clientToServer)
    {
    char tempFolderPath[MAX_PATH];
    GetTempPathA(MAX_PATH, tempFolderPath);

    std::string path;
    path.append(tempFolderPath);
    path.append(clientToServer ? BLENDERSYNC_FS_CLIENT_TO_SERVER_DATA_FILE_NAME : BLENDERSYNC_FS_SERVER_TO_CLIENT_DATA_FILE_NAME);

    return path;
    }

    bool fileExists(const std::string& filePath)
    {
    bool found = false;
    DWORD dwAttrib = GetFileAttributes(filePath.c_str());
    found = (dwAttrib != INVALID_FILE_ATTRIBUTES) && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
    return found;
    }

    // When using filesystem communication, this returns true if the client has sent data to the server.
    // After returning true, the data can be found in gTempBuffer.
    bool readDataFromClientFS(size_t& dataLength)
    {
    std::string filePath = getFSCommDataFilePath(true);

    if (fileExists(filePath))
    {
    std::ifstream file(filePath.c_str(), std::ios::in | std::ios::binary | std::ios::ate);

    if (!file.is_open())
    {
    return false;
    }

    dataLength = file.tellg();
    file.seekg(0, std::ios::beg);
    file.read((char*)gTempBuffer, dataLength);
    file.close();

    DeleteFile(filePath.c_str());

    return true;
    }

    return false;
    }

    void writeDataToClientFS(uint8_t* data, size_t dataLength)
    {
    std::string filePath = getFSCommDataFilePath(false);

    std::ofstream outfile(filePath.c_str(), std::ofstream::binary);

    if (outfile.fail())
    {
    printf("ERROR writing data to file: %s\n", filePath.c_str());
    return;
    }

    outfile.write((const char*)data, dataLength);

    if (outfile.bad())
    {
    printf("ERROR writing data to file: %s\n", filePath.c_str());
    return;
    }

    outfile.close();
    }
    #endif // BLENDERSYNC_FILESYSTEM_COMMS_ENABLED

    uint32_t readUint(enet_uint8* data, uint32_t index)
    {
    // This is little-endian:
    return data[index] + (data[index + 1] << 8) + (data[index + 2] << 16) + (data[index + 3] << 24);

    // This is big-endian:
    //return (data[index] << 24) + (data[index + 1] << 16) + (data[index + 2] << 8) + data[index + 3];
    }

    void writeUint(enet_uint8* data, uint32_t index, uint32_t value)
    {
    // This is little-endian:
    data[index + 0] = (value) & 0xFF;
    data[index + 1] = (value >> 8) & 0xFF;
    data[index + 2] = (value >> 16) & 0xFF;
    data[index + 3] = (value >> 24) & 0xFF;
    }

    float readFloat(enet_uint8* data, uint32_t index)
    {
    union FloatUnion
    {
    float floatValue;
    enet_uint8 bytes[4];
    };

    FloatUnion u;

    for (int i = 0; i < 4; ++i)
    u.bytes[i] = data[index + i];

    return u.floatValue;
    }

    int parseData(enet_uint8* data, size_t dataLength)
    {
    /*
    Data protocol:
    Byte 0: Data packet type (uint8)
    Bytes 1 - 4: Edit ID (uint32)
    Bytes 5 - 8: Target object rotation W (float)
    Bytes 9 - 12: Target object rotation X (float)
    Bytes 13 - 16: Target object rotation Y (float)
    Bytes 17 - 20: Target object rotation Z (float)
    Bytes 21 - 24: Target mesh path length (uint32)
    Bytes: 25 - N: Target mesh path (string data, UTF-8)
    Bytes: N+1 - N+4: Surroundings mesh path length(uint32)
    Bytes: N+5 - EOD: Surroundings mesh path (string data, UTF-8)
    */

    uint8_t dataPacketType = data[0];
    uint32_t cursor = 1; // Move to the second byte.

    gEditID = readUint(data, cursor);
    cursor += 4;

    gTargetObjectRotationW = readFloat(data, cursor);
    cursor += 4;

    gTargetObjectRotationX = readFloat(data, cursor);
    cursor += 4;

    gTargetObjectRotationY = readFloat(data, cursor);
    cursor += 4;

    gTargetObjectRotationZ = readFloat(data, cursor);
    cursor += 4;

    if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH ||
    dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH)
    {
    // Target mesh path received:
    uint32_t pathLength = readUint(data, cursor);
    cursor += 4;

    if (pathLength > 0)
    {
    strncpy_s(gTargetMeshFilePath, (const char*)(data + cursor), pathLength);
    cursor += pathLength;
    }
    else
    {
    strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), "");
    }

    if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH)
    {
    return BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED;
    }
    }

    if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH)
    {
    // Target + surroundings mesh path received:
    uint32_t pathLength = readUint(data, cursor);
    cursor += 4;

    strncpy_s(gSurroundingsMeshFilePath, (const char*)(data + cursor), pathLength);
    cursor += pathLength;

    return BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED;
    }

    return BLENDERSYNC_EVENT_NONE;
    }

    extern "C" BLENDERSYNCLIBAPI int initBlenderSyncLib()
    {
    if (enet_initialize() != 0)
    {
    return 1; // Error initializing ENet.
    }

    strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), "");
    strcpy_s(gSurroundingsMeshFilePath, sizeof(gSurroundingsMeshFilePath), "");

    return 0;
    }

    extern "C" BLENDERSYNCLIBAPI void deinitBlenderSyncLib()
    {
    enet_deinitialize();
    }

    extern "C" BLENDERSYNCLIBAPI int startServer(int port)
    {
    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    gRunningWithFilesystemComms = false;
    #endif

    ENetAddress addr;
    addr.host = ENET_HOST_ANY;
    addr.port = port;

    size_t maxPeerCount = 1;
    size_t channelLimit = 0; // 0 means maximum number of allowed channels

    gEnetHost = enet_host_create(&addr, maxPeerCount, channelLimit, 0, 0);

    if (!gEnetHost)
    {
    return 1; // Error.
    }

    return 0; // Success.
    }

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    extern "C" BLENDERSYNCLIBAPI int startServerFS()
    {
    gRunningWithFilesystemComms = true;

    // Delete any communications data files from a previous session, left in the temp folder.

    {
    std::string filePath = getFSCommDataFilePath(true);
    if (fileExists(filePath)) DeleteFile(filePath.c_str());
    }

    {
    std::string filePath = getFSCommDataFilePath(false);
    if (fileExists(filePath)) DeleteFile(filePath.c_str());
    }

    return 0; // Success.
    }
    #endif

    extern "C" BLENDERSYNCLIBAPI void stopServer()
    {
    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (!gRunningWithFilesystemComms && !gEnetHost)
    #else
    if (!gEnetHost)
    #endif
    {
    return;
    }

    if (gEnetClientPeer)
    {
    ENetEvent event;
    enet_peer_disconnect(gEnetClientPeer, 0);

    // Allow up to 3 seconds for the disconnect to succeed and drop any packets received packets.
    while (enet_host_service(gEnetHost, &event, 3000) > 0)
    {
    switch (event.type)
    {
    case ENET_EVENT_TYPE_RECEIVE:
    enet_packet_destroy(event.packet);
    break;
    case ENET_EVENT_TYPE_DISCONNECT:
    gEnetClientPeer = nullptr;
    break;
    }
    }

    if (gEnetClientPeer)
    {
    // We've arrived here, so the disconnect attempt didn't succeed yet. Force the connection down.
    enet_peer_reset(gEnetClientPeer);
    gEnetClientPeer = nullptr;
    }
    }

    if (gEnetHost)
    {
    enet_host_destroy(gEnetHost);
    gEnetHost = nullptr;
    }

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    gRunningWithFilesystemComms = false;
    #endif
    }

    extern "C" BLENDERSYNCLIBAPI int isServerRunning()
    {
    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    return (gRunningWithFilesystemComms || gEnetHost != nullptr) ? 1 : 0;
    #else
    return (gEnetHost != nullptr) ? 1 : 0;
    #endif
    }

    extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationW()
    {
    return gTargetObjectRotationW;
    }

    extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationX()
    {
    return gTargetObjectRotationX;
    }

    extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationY()
    {
    return gTargetObjectRotationY;
    }

    extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationZ()
    {
    return gTargetObjectRotationZ;
    }

    extern "C" BLENDERSYNCLIBAPI const char* getTargetMeshFilePath()
    {
    return gTargetMeshFilePath;
    }

    extern "C" BLENDERSYNCLIBAPI const char* getSurroundingsMeshFilePath()
    {
    return gSurroundingsMeshFilePath;
    }

    extern "C" BLENDERSYNCLIBAPI int sendUpdatedMeshFilePaths(const char* workFileZipPath, int workFileZipPathLength, const char* binmeshFilePath, int binmeshFilePathLength, bool keepEditing)
    {
    printf("BlenderSyncLib: workFileZipPath: %s, length: %d\n", workFileZipPath, workFileZipPathLength);
    printf("BlenderSyncLib: binmeshFilePath: %s, length: %d\n", binmeshFilePath, binmeshFilePathLength);

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (gRunningWithFilesystemComms || gEnetClientPeer)
    #else
    if (gEnetClientPeer)
    #endif
    {
    gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_TARGET_MESH_UPDATED;
    uint32_t cursor = 1; // Move to the second byte.

    writeUint(gTempBuffer, cursor, gEditID);
    cursor += 4;

    // One byte specifying whether or not to keep editing.
    gTempBuffer[cursor] = keepEditing ? 1 : 0;
    cursor += 1;

    // Writing the paths prepended with the length as uint32_ts means that we can "get" them out of the stream as Basis strings on the C++ side.

    writeUint(gTempBuffer, cursor, (uint32_t)workFileZipPathLength);
    cursor += 4;
    memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, workFileZipPath, workFileZipPathLength);
    cursor += workFileZipPathLength;

    writeUint(gTempBuffer, cursor, (uint32_t)binmeshFilePathLength);
    cursor += 4;
    memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, binmeshFilePath, binmeshFilePathLength);
    cursor += binmeshFilePathLength;

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (gRunningWithFilesystemComms)
    {
    writeDataToClientFS(gTempBuffer, cursor);
    }
    else
    #endif
    {
    ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE);
    enet_peer_send(gEnetClientPeer, 0, packet);
    enet_host_flush(gEnetHost);
    }

    if (!keepEditing)
    {
    // The edit is officially "over" now, so reset the ID.
    gEditID = 0xFFFFFFFF;
    }

    return 0;
    }

    return 1; // No peer connected, cannot send.
    }

    extern "C" BLENDERSYNCLIBAPI int cancelEdit()
    {
    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (gRunningWithFilesystemComms || gEnetClientPeer)
    #else
    if (gEnetClientPeer)
    #endif
    {
    printf("BlenderSyncLib: cancelEdit()\n");

    gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_CANCEL_EDIT;
    uint32_t cursor = 1; // Move to the second byte.

    writeUint(gTempBuffer, cursor, gEditID);
    cursor += 4;

    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (gRunningWithFilesystemComms)
    {
    writeDataToClientFS(gTempBuffer, cursor);
    }
    else
    #endif
    {
    ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE);
    enet_peer_send(gEnetClientPeer, 0, packet);
    enet_host_flush(gEnetHost);
    }

    gEditID = 0xFFFFFFFF;

    return 0;
    }

    return 1; // No peer connected, cannot send.
    }

    extern "C" BLENDERSYNCLIBAPI int service()
    {
    #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED
    if (gRunningWithFilesystemComms)
    {
    size_t dataLength = 0;
    if (readDataFromClientFS(dataLength))
    {
    printf("BlenderSyncLib: Data received (via filesystem), %u bytes\n", (int)dataLength);

    int returnCode = parseData(gTempBuffer, dataLength);
    return returnCode;
    }
    }
    else
    #endif
    if (gEnetHost)
    {
    ENetEvent enetEvent;
    uint32_t timeoutMs = 0;

    int res = enet_host_service(gEnetHost, &enetEvent, timeoutMs);

    if (res > 0)
    {
    switch (enetEvent.type)
    {
    case ENET_EVENT_TYPE_CONNECT:
    gEnetClientPeer = enetEvent.peer;

    // Set longer timeouts.
    enet_peer_timeout(gEnetClientPeer, BLENDERSYNC_PEER_TIMEOUT_LIMIT,
    BLENDERSYNC_PEER_TIMEOUT_MINIMUM, BLENDERSYNC_PEER_TIMEOUT_MAXIMUM);

    printf("BlenderSyncLib: Client connected.\n");
    return BLENDERSYNC_EVENT_CLIENT_CONNECTED;
    case ENET_EVENT_TYPE_DISCONNECT:
    gEnetClientPeer = nullptr;
    printf("BlenderSyncLib: Client disconnected.\n");
    return BLENDERSYNC_EVENT_CLIENT_DISCONNECTED;
    case ENET_EVENT_TYPE_RECEIVE:
    {
    printf("BlenderSyncLib: Data received, %u bytes\n", (int)enetEvent.packet->dataLength);

    int returnCode = parseData(enetEvent.packet->data, enetEvent.packet->dataLength);

    enet_packet_destroy(enetEvent.packet);
    return returnCode;
    }
    default:
    return BLENDERSYNC_EVENT_NONE;
    }
    }
    else //if (res == 0)
    {
    return BLENDERSYNC_EVENT_NONE;
    }
    /*else
    {
    // A negative value indicates an error.
    }*/
    }

    return BLENDERSYNC_EVENT_NONE;
    }
    619 changes: 619 additions & 0 deletions blendersync.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,619 @@
    import bpy
    import ctypes
    import tempfile
    import pathlib
    import subprocess
    import zipfile
    import os
    import mathutils

    blender_sync_lib = None
    server_running = False
    server_shutdown_requested = False

    editing_mesh = False

    surroundings_obj_file_path = ""

    # Update the server four times per second.
    server_update_rate = 0.25

    # These need to match the values on the C side:
    BLENDERSYNC_EVENT_NONE = 0
    BLENDERSYNC_EVENT_CLIENT_CONNECTED = 1
    BLENDERSYNC_EVENT_CLIENT_DISCONNECTED = 2
    BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED = 3
    BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED = 4

    BLENDERSYNC_TARGET_OBJECT_NAME = "BlenderSyncTarget"
    BLENDERSYNC_TARGET_OBJECT_DATA_NAME = "BlenderSyncTargetData"

    BLENDERSYNC_SURROUNDINGS_OBJECT_NAME = "BlenderSyncSurroundingsObj"

    BLENDERSYNC_HELPERS_COLLECTION_NAME = "BlenderSyncHelpers"

    BLENDERSYNC_ZIP_BLEND_ARCNAME = "blendersync_target.blend"
    BLENDERSYNC_ZIP_FBX_ARCNAME = "blendersync_target.fbx"

    #server_iteration = 0

    def get_addon_name():
    return __package__.split('.')[0]

    def update_server():
    global server_running
    global server_shutdown_requested
    global editing_mesh
    global surroundings_obj_file_path
    #global server_iteration

    #print(f"server iteration: {server_iteration}")
    #server_iteration += 1

    if server_shutdown_requested:
    print("BasisBlenderSync: Shutting down server...")
    server_running = False
    server_shutdown_requested = False
    editing_mesh = False
    blender_sync_lib.stopServer()
    return None

    evt = blender_sync_lib.service()

    if evt == BLENDERSYNC_EVENT_NONE:
    pass
    elif evt == BLENDERSYNC_EVENT_CLIENT_CONNECTED:
    print("BasisBlenderSync: client connected")
    elif evt == BLENDERSYNC_EVENT_CLIENT_DISCONNECTED:
    print("BasisBlenderSync: client disconnected")
    elif (evt == BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED or
    evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED):
    target_mesh_path = get_target_mesh_file_path()
    editing_mesh = True
    #print(f"Received target path: {target_mesh_path}")

    if target_mesh_path == "":
    print("BasisBlenderSync: Creating new scene.")
    create_new_mesh()
    else:
    print(f"BasisBlenderSync: Loading previous scene: {target_mesh_path}")
    open_mesh(target_mesh_path)

    if evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED:
    surroundings_obj_file_path = get_surroundings_mesh_file_path()
    print(f"BasisBlenderSync: Importing surroundings from: {surroundings_obj_file_path}")
    import_surroundings_mesh(surroundings_obj_file_path, False)
    #print(f"Received target path: {target_mesh_path}, and surroundings path: {surroundings_obj_file_path}")
    else:
    surroundings_obj_file_path = ""

    return server_update_rate

    def get_target_mesh_file_path():
    b = blender_sync_lib.getTargetMeshFilePath()
    return b.decode('UTF-8')

    def get_surroundings_mesh_file_path():
    b = blender_sync_lib.getSurroundingsMeshFilePath()
    return b.decode('UTF-8')

    #############################

    # Commented out, this is used for adding a panel to the "N-menu" on the right of the view.

    # class VIEW3D_PT_blendersync(bpy.types.Panel):
    # bl_space_type = "VIEW_3D"
    # bl_region_type = "UI"
    # bl_category = "BlenderSync"
    # bl_label = "Basis BlenderSync"

    # def draw(self, context):
    # global server_running

    # if server_running:
    # self.layout.label(text="Server running", icon="FUND") # FUND is a heart.
    # self.layout.operator("object.stop_blendersync_server")
    # else:
    # self.layout.label(text="Server stopped")
    # self.layout.operator("object.start_blendersync_server")

    # self.layout.separator()

    # col = self.layout.column(align=True)
    # col.enabled = server_running
    # col.operator("object.finish_editing_blendersync_object")

    class VIEW3D_PT_blendersync_button(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "HEADER"
    #bl_category = "BlenderSync"
    bl_label = "Basis BlenderSync"

    @staticmethod
    def draw_popover(self, context):
    global server_running
    global editing_mesh

    self.layout.separator()

    if editing_mesh:
    # We use the ERROR icon here since it is a warning sign. There is no error, it's just to notify the
    # user that we are editing the mesh at the moment.
    self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="FILE_3D")
    else:
    if server_running:
    self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="LINKED")
    else:
    self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="UNLINKED")

    def draw(self, context):
    global server_running
    global editing_mesh

    layout = self.layout

    if server_running:
    layout.label(text="Server running", icon="LINKED")
    layout.operator("object.stop_blendersync_server", icon="SNAP_FACE") # SNAP_FACE looks like a stop sign.
    else:
    layout.label(text="Server stopped", icon="UNLINKED")
    layout.operator("object.start_blendersync_server", icon="PLAY")

    if editing_mesh:
    layout.separator()

    col = layout.column(align=True)
    col.label(text="Currently editing an object")
    col.operator(FinishEditingBlenderSyncObject.bl_idname, text="Finish editing", icon="CHECKMARK")
    col.operator(SendBlenderSyncDataAndKeepEditing.bl_idname, text="Update and keep editing", icon="UV_SYNC_SELECT")
    col.separator()
    col.operator(CancelBlenderSyncEdit.bl_idname, text="Cancel edit", icon="CANCEL")
    col.separator()
    col.operator(CreateHelpersCollectionOperator.bl_idname, text="Create helpers collection", icon="OUTLINER_COLLECTION")

    ##################################################################

    class AddonPreferences(bpy.types.AddonPreferences):
    bl_idname = get_addon_name()

    server_port_number: bpy.props.IntProperty(name="Port number",
    description="The UDP port number to start the server on.",
    min=0,
    max=65535,
    default=1950)

    basis_mesh_converter_path: bpy.props.StringProperty(name="Mesh converter exe path",
    description="The path to the Basis mesh converter exe.",
    default="")

    def draw(self, context):
    self.layout.label(text="These are the preferences for Basis BlenderSync.")
    self.layout.prop(self, "server_port_number")
    self.layout.prop(self, "basis_mesh_converter_path")

    def get_addon_prefs(context):
    preferences = context.preferences
    addon_prefs = preferences.addons[get_addon_name()].preferences
    return addon_prefs

    ##################################################################

    # Operators for starting/stopping the server.

    class StartServerOperator(bpy.types.Operator):
    bl_idname = "object.start_blendersync_server"
    bl_label = "Start Server"

    def execute(self, context):
    global server_running
    global server_shutdown_requested

    server_shutdown_requested = False
    addon_prefs = get_addon_prefs(context)

    if addon_prefs.basis_mesh_converter_path == "":
    self.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.")
    return {'FINISHED'}

    print(f"Starting server on port {addon_prefs.server_port_number}")
    s = blender_sync_lib.startServer(addon_prefs.server_port_number)

    if s == 0:
    # Kick off the server update loop. persistent=True means that
    # the timer keeps running even after a new blend file is loaded.
    bpy.app.timers.register(update_server, persistent=True)
    server_running = True
    else:
    self.report({'ERROR'}, "The server failed to start.")

    return {'FINISHED'}

    class StopServerOperator(bpy.types.Operator):
    bl_idname = "object.stop_blendersync_server"
    bl_label = "Stop Server"

    def execute(self, context):
    global server_running
    global server_shutdown_requested
    server_shutdown_requested = True
    server_running = False
    return {'FINISHED'}

    ##################################################################

    # Operator for sending the selected object back to Basis.

    class FinishEditingBlenderSyncObject(bpy.types.Operator):
    bl_idname = "object.finish_editing_blendersync_object"
    bl_label = "Finish editing object"

    def execute(self, context):
    global server_running
    global editing_mesh

    if not server_running:
    self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.")
    return {'FINISHED'}

    keep_editing = False
    if send_mesh_data_to_basis(self, context, keep_editing):
    editing_mesh = False
    return {'FINISHED'}

    class SendBlenderSyncDataAndKeepEditing(bpy.types.Operator):
    bl_idname = "object.send_blendersync_data_and_keep_editing"
    bl_label = "Send data to Basis and keep editing"

    def execute(self, context):
    global server_running

    if not server_running:
    self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.")
    return {'FINISHED'}

    keep_editing = True
    send_mesh_data_to_basis(self, context, keep_editing)
    return {'FINISHED'}

    import bpy

    class CancelBlenderSyncEdit(bpy.types.Operator):
    bl_idname = "object.cancel_blendersync_edit"
    bl_label = "Really cancel the edit?"
    bl_options = {'REGISTER', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
    return True

    def execute(self, context):
    cancel_edit(self)
    return {'FINISHED'}

    def invoke(self, context, event):
    return context.window_manager.invoke_confirm(self, event)

    class CreateHelpersCollectionOperator(bpy.types.Operator):
    bl_idname = "object.create_blendersync_helper_collection"
    bl_label = "Create Helpers Collection"

    def execute(self, context):
    create_helpers_collection()
    return {'FINISHED'}

    ##################################################################

    # # Operator for clearing the scene.

    # class DeleteObjectsOperator(bpy.types.Operator):
    # bl_idname = "object.blendersync_clear_scene"
    # bl_label = "Delete Objects (BlenderSync)"

    # def execute(self, context):
    # if bpy.context.object.mode == 'EDIT':
    # bpy.ops.object.mode_set(mode='OBJECT')
    # # deselect all objects
    # bpy.ops.object.select_all(action='SELECT')
    # # delete all selected objects
    # bpy.ops.object.delete()
    # return {'FINISHED'}

    # class TestCreateNewMesh(bpy.types.Operator):
    # bl_idname = "object.test_blendersync_create_new_mesh"
    # bl_label = "Test Create New BlenderSync Mesh"

    # def execute(self, context):
    # #print(context.selected_objects)
    # create_new_mesh()
    # return {'FINISHED'}

    # class TestSaveBlenderSyncToTempFile(bpy.types.Operator):
    # bl_idname = "object.test_blendersync_save_to_temp_file"
    # bl_label = "Test Save Blender Sync To Temp File"

    # def execute(self, context):
    # finish_editing_mesh(self, context)
    # return {'FINISHED'}

    ##################################################################

    # Function for reading/writing BlenderSync meshes.

    def create_helpers_collection():
    # If the helpers collection isn't already there, create and link it.
    if bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME) == None:
    coll = bpy.data.collections.new(BLENDERSYNC_HELPERS_COLLECTION_NAME)
    bpy.context.scene.collection.children.link(coll)

    def create_new_mesh():
    bpy.ops.wm.read_homefile(use_empty=True)
    bpy.ops.mesh.primitive_cube_add()
    target_obj = bpy.context.scene.objects[0]
    target_obj.name = BLENDERSYNC_TARGET_OBJECT_NAME
    target_obj.data.name = BLENDERSYNC_TARGET_OBJECT_DATA_NAME
    set_target_object_rotation(target_obj)

    def open_mesh(zipped_work_file_path):
    containing_folder = str(pathlib.Path(zipped_work_file_path).parent)
    archive = zipfile.ZipFile(zipped_work_file_path, 'r')
    blend_file_path = archive.extract(BLENDERSYNC_ZIP_BLEND_ARCNAME, path=containing_folder)
    #print(f"Extracted blend file: {blend_file_path}")
    bpy.ops.wm.open_mainfile(filepath=blend_file_path)
    target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
    target_obj.select_set(True)
    bpy.context.view_layer.objects.active = target_obj
    set_target_object_rotation(target_obj)
    print(f"BasisBlenderSync: Opened blend file: {blend_file_path}")

    def import_surroundings_mesh(surroundings_obj_file_path, hide_surroundings):
    # Activate the master collection.
    bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection

    # Use the new obj importer, starting with Blender 4.0.
    if (bpy.app.version[0] >= 4):
    # We need the context.temp_override stuff, otherwise we get
    # 'RuntimeError: Operator bpy.ops.wm.obj_import.poll() failed, context is incorrect'
    with bpy.context.temp_override(window=bpy.data.window_managers[0].windows[0]):
    bpy.ops.wm.obj_import(filepath=surroundings_obj_file_path)
    else:
    bpy.ops.import_scene.obj(filepath=surroundings_obj_file_path)

    helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME)

    # Any and all objects in the scene that are not the target object nor a
    # helper object are considered to be part of the surroundings
    for obj in bpy.context.scene.objects:
    if (helpers_coll != None) and (obj in list(helpers_coll.objects)):
    continue

    if obj.name != BLENDERSYNC_TARGET_OBJECT_NAME:
    obj.name = BLENDERSYNC_SURROUNDINGS_OBJECT_NAME
    obj.hide_set(hide_surroundings)
    #obj.hide_viewport = hide_surroundings

    # Re-select the target object.
    bpy.ops.object.select_all(action='DESELECT')
    target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
    target_obj.select_set(True)
    bpy.context.view_layer.objects.active = target_obj

    # Sends the mesh data to Basis and returns whether or not the operation succeeded.
    def send_mesh_data_to_basis(op, context, keep_editing):
    if bpy.context.object.mode != 'OBJECT':
    bpy.ops.object.mode_set(mode='OBJECT')

    surroundings_object_hidden = False

    target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME)
    surroundings_obj = bpy.context.scene.objects.get(BLENDERSYNC_SURROUNDINGS_OBJECT_NAME)

    if not target_obj:
    op.report({'ERROR'}, f"Could not find BlenderSync target object: '{BLENDERSYNC_TARGET_OBJECT_NAME}'")
    return False

    if surroundings_obj:
    surroundings_object_hidden = not surroundings_obj.visible_get()
    print(f"BasisBlenderSync: Surroundings object found, hidden: {surroundings_object_hidden}")

    # Unhide all objects.
    for obj in bpy.context.scene.objects:
    obj.hide_set(False)
    #obj.hide_viewport = False

    world_origin = mathutils.Vector([0, 0, 0])
    distance_to_origin = (target_obj.location - world_origin).length

    if distance_to_origin > 0.0001:
    op.report({'ERROR'}, "The target object pivot is not at the origin.")
    return False

    # Should be able to use this to "apply all transforms" to the target object if we want. (untested)
    #bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)

    # Reset the rotation of the target object.
    target_obj.rotation_mode = 'XYZ'
    target_obj.rotation_euler = [0, 0, 0]

    # If we are in local-view (numpad '/') we need to toggle out of it before proceeding.
    # Otherwise the attempt to delete the surroundings object(s) fails.
    if context.space_data.local_view is not None:
    bpy.ops.view3d.localview()
    #print("Toggled out of local-view.");

    # Deselect all objects.
    bpy.ops.object.select_all(action='DESELECT')

    # If we have a helpers collection, select every object in it.
    helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME)
    if not helpers_coll is None:
    for helper_obj in helpers_coll.objects:
    helper_obj.select_set(True)

    # Make the target the active object.
    #bpy.context.view_layer.objects.active = target_obj

    # Select the target object and invert the selection.
    target_obj.select_set(True)
    bpy.ops.object.select_all(action='INVERT')

    # Delete the selected object(s), ie. all objects except the target (and any helper objects).
    bpy.ops.object.delete()

    # Remove all unused data.

    for block in bpy.data.meshes:
    if block.users == 0:
    bpy.data.meshes.remove(block)

    for block in bpy.data.materials:
    if block.users == 0:
    bpy.data.materials.remove(block)

    for block in bpy.data.images:
    if block.users == 0:
    bpy.data.images.remove(block)

    for block in bpy.data.textures:
    if block.users == 0:
    bpy.data.textures.remove(block)

    # Select the target object again.
    target_obj.select_set(True)

    # Generate a writable temp file name by creating a temp file and appending the extensions as appropriate.
    fo = tempfile.NamedTemporaryFile()
    temp_file_path = fo.name
    fo.close()

    work_file_path = temp_file_path + ".blend"
    zipped_work_file_path = temp_file_path + ".zip"
    exported_file_path = temp_file_path + ".fbx"
    exported_file_meta_path = temp_file_path + ".fbx.meta"
    converted_file_path = temp_file_path + ".binmesh"
    converter_metadata_file_path = temp_file_path + ".meta"

    # Save the blender scene to a temp file. copy=True saves the file but doesn't make it the active document.
    bpy.ops.wm.save_as_mainfile(filepath=work_file_path, copy=True)
    print(f"BasisBlenderSync: Work file saved to '{work_file_path}'")

    # Export the target object to a temp fbx file.

    # bpy.ops.export_scene.fbx(filepath='', check_existing=True, filter_glob='*.fbx', use_selection=False,
    # use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE',
    # use_space_transform=True, bake_space_transform=False,
    # object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, use_mesh_modifiers=True,
    # use_mesh_modifiers_render=True, mesh_smooth_type='OFF', use_subsurf=False, use_mesh_edges=False,
    # use_tspace=False, use_custom_props=False, add_leaf_bones=True, primary_bone_axis='Y', secondary_bone_axis='X',
    # use_armature_deform_only=False, armature_nodetype='NULL', bake_anim=True, bake_anim_use_all_bones=True,
    # bake_anim_use_nla_strips=True, bake_anim_use_all_actions=True, bake_anim_force_startend_keying=True,
    # bake_anim_step=1.0, bake_anim_simplify_factor=1.0, path_mode='AUTO', embed_textures=False, batch_mode='OFF',
    # use_batch_own_dir=True, use_metadata=True, axis_forward='-Z', axis_up='Y')

    bpy.ops.export_scene.fbx(filepath=exported_file_path,
    use_selection=True, object_types={'MESH'}, axis_forward='Z')

    print(f"BasisBlenderSync: Object exported to '{exported_file_path}'")

    # Write a meta file to instruct the Basis mesh converter what to do:

    # Example:
    # {
    # "vertexFormat":"PositionTangentBinormalNormalTexcoord",
    # "generatePhysicsTriMesh":false,
    # "linearVertexColors":false,
    # "optimizeGeometry":true,
    # "resourceTags":[]
    # }

    exported_mesh_metadata_json = '{"generatePhysicsTriMesh":true}'
    with open(exported_file_meta_path, 'w') as outfile:
    outfile.write(exported_mesh_metadata_json)

    # Make sure we have the Basis mesh converter path set up.

    addon_prefs = get_addon_prefs(context)
    print(f"BasisBlenderSync: Mesh converter path: {addon_prefs.basis_mesh_converter_path}")
    #s = blender_sync_lib.startServer(addon_prefs.server_port_number)

    if addon_prefs.basis_mesh_converter_path == "":
    op.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.")
    return False

    mesh_converter_path = pathlib.Path(addon_prefs.basis_mesh_converter_path)

    if not mesh_converter_path.exists():
    op.report({'ERROR'}, f"The Basis mesh converter exe path {addon_prefs.basis_mesh_converter_path} is not valid.")
    return False

    # Convert the fbx file to a binmesh.
    conversion_command = f'{addon_prefs.basis_mesh_converter_path} -i:"{exported_file_path}" -o:"{converted_file_path}" -m:"{converter_metadata_file_path}" -p:physx'
    conversion_result = subprocess.run(conversion_command, shell=True)
    print(f"BasisBlenderSync: Object converted to '{converted_file_path}'")

    if conversion_result.returncode != 0:
    op.report({'ERROR'}, f"Error converting the object to a Basis mesh. Check the Blender console for more info.")
    return False

    # Write the blend file to a zip archive.
    with zipfile.ZipFile(zipped_work_file_path, 'w') as zipObj:
    zipObj.write(work_file_path, arcname=BLENDERSYNC_ZIP_BLEND_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
    zipObj.write(exported_file_path, arcname=BLENDERSYNC_ZIP_FBX_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)

    # Remove the files we don't need anymore.
    os.remove(work_file_path)
    os.remove(exported_file_path)

    workFileZipPathBytes = zipped_work_file_path.encode('UTF-8')
    binmeshFilePathBytes = converted_file_path.encode('UTF-8')

    if not server_running:
    op.report({'ERROR'}, f"The BlenderSync server is not running. Cannot send the mesh data to Basis.")
    return False

    if blender_sync_lib.sendUpdatedMeshFilePaths(workFileZipPathBytes, len(workFileZipPathBytes),
    binmeshFilePathBytes, len(binmeshFilePathBytes), keep_editing) == 0:
    op.report({'INFO'}, "Successfully sent the mesh data to the Basis editor.")
    else:
    op.report({'ERROR'}, f"Error sending the mesh data to Basis. Is the client connected?")

    if keep_editing:
    # The user wants to keep editing.
    open_mesh(zipped_work_file_path)
    if surroundings_obj_file_path != "":
    import_surroundings_mesh(surroundings_obj_file_path, surroundings_object_hidden)
    else:
    # The user does not want to keep editing. Clear the scene.
    bpy.ops.wm.read_homefile(use_empty=True)

    return True

    def set_target_object_rotation(target_obj):
    w = blender_sync_lib.getTargetObjectRotationW()
    x = blender_sync_lib.getTargetObjectRotationX()
    y = blender_sync_lib.getTargetObjectRotationY()
    z = blender_sync_lib.getTargetObjectRotationZ()

    print(f"BasisBlenderSync: Target rotation W: {w}")
    print(f"BasisBlenderSync: Target rotation X: {x}")
    print(f"BasisBlenderSync: Target rotation Y: {y}")
    print(f"BasisBlenderSync: Target rotation Z: {z}")

    target_obj.rotation_mode = 'QUATERNION'

    # This is how we convert from a Basis quaternion to a Blender quaternion:
    target_obj.rotation_quaternion = mathutils.Quaternion((-w, x, z, -y))

    # Reset the rotation mode.
    target_obj.rotation_mode = 'XYZ'

    def cancel_edit(op):
    global editing_mesh
    editing_mesh = False

    if blender_sync_lib.cancelEdit() == 0:
    op.report({'INFO'}, "Successfully canceled the mesh editing.")
    else:
    op.report({'ERROR'}, f"Error sending the cancel message to Basis. Is the client connected?")

    bpy.ops.wm.read_homefile(use_empty=True)