If you have ever used Quixel Bridge, Substance Painter or Unreal Engine, you would have come across their live link feature at some point. Live links make your life easier by handling the transportation of data between to applications that are incompatible out of the box. For example, the Megascans Addon for Blender allows you to export an asset from Quixel Bridge, import it into Blender and properly setup their materials all in a single click.

Blender FreeCAD Live Link

These experiences are enabled by building live links between these applications using their respective APIs. But how do they work? What if you have an application from which you want to send data to Blender, but there is no live link for it? How difficult is it to build one?

We will explore answers to these question in this article.

Live links are made possible by a technology called sockets – the fundamental building blocks of networks and web applications. Sockets enable two processes (or applications) to communicate with each other by sharing byte streams. The processes could be running on two different machines or on the same machine. In case of live links, both applications are going to be on the same machine with a socket of their own.

All sockets posses the same functionality, but based on how you create and use them, they either act like a client or a server. Usually, the application on the receiving end of the will act like a server, because it has to constantly listen for connections from the application which is going to send the data. And the application that is going to send the data acts like the client, because it can open a connection to the server socket (receiving application) whenever it wants to send some data, and can close the connection once it’s done (just like a web browser).

If you consider the Blender-Unreal live link, it lets you send models from Blender to Unreal and vice versa. But what is happening under the hood?

Lets say you are sending a model from Unreal to Blender using the live link to make some changes to the model. What happens under the hood is, Unreal is exporting the model to a format that Blender can import (say FBX), shares the full path to the exported model with Blender by establishing a socket connection, and once Blender receives the full path, it then imports the model. Also, a socket will be open in Blender indefinitely to listen for incoming data from Unreal.

Now, this is an oversimplification and obviously there is more to it, but this is the general idea behind live links. Also, apart from the path of the model, other data could be passed around too. Like, Unreal can tell which is the front axis and the up axis so that Blender could import the model in the right orientation.

So basically, live links work by leveraging existing features of the applications and by passing around some metadata (location of exported model, orientation data, scale, etc.) about the data that is being exchanged.

For the purpose of this article, the application at the receiving end is going to be Blender, and the application which will be sending data through our live link is going to be FreeCAD – an open source CAD application. But once you understand the ideas in this article, you can implement live links between any two applications, provided they have an API to access their internal data and to create sockets.

Our live link will take the model currently open in FreeCAD, export it to a format Blender can import (say OBJ), import it into Blender basic with materials and colors. So as a end user, you can imagine a user experience something like this:

  1. Open the model in FreeCAD.
  2. Click on the Blender menu from FreeCAD menu bar (which we will add).
  3. Select Export to Blender from the menu to initiate the live link.
  4. The model is automatically imported into the running instance of Blender in the right scale, orientation and color.

In order to enable this user experience, we need our client and server sockets (will be running in FreeCAD and Blender respectively) to exchange some data. In this case, the data we need to exchange is the location of the exported model from FreeCAD. Lets get going!

Note: I will be using FreeCAD 0.20.2 and Blender 3.3 LTS (3.3.7). Kindly use these versions if you face any issues when you follow along.

1. Setting up the socket connection

Before we get to the details like exporting, importing, etc. we get the crux of the problem out of the way – establishing a socket connection between FreeCAD and Blender. By the way, by no means this is a socket programming tutorial. If you are looking for a comprehensive introduction to sockets, I recommend reading this Real Python article.

With that being said, lets open FreeCAD and try out the following snippet of code in the Python Console:

# FreeCAD socket setup (client)

import socket

def say_hello():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    client_socket.connect(server_address)
    client_socket.sendall("Hello, Blender!".encode())
    success_message = client_socket.recv(1024).decode()
    print("Blender:", success_message)
    client_socket.close()

The say_hello() function creates socket object, establishes a connection with the server running on port 25000 on the same machine (hence localhost), sends a hello message to it, waits for a success message, prints out the success message and closes the connection. The 1024 inside the recv() method denotes the length of the message that can be received in a single go. In this case, 1024 characters is plenty to share file paths.

Now, you cannot run this code yet, since there is no server running on the port 25000. So lets setup the server socket in Blender. Note that, in order to be able to see the data getting printed in the console (in the next step), you need to start Blender from the terminal. Alternatively, if you are on Windows, you can click on Window > Toggle System Console from the menu bar to bring up the console window after launching. Refer to this page in the Blender documentation for more details.

Fire up Blender, go to the Scripting tab, create a new text file in the Text Editor and copy paste the following code:

import socket

def receive_data():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for data...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()
        print(f"Data received: {data}")
        connection.sendall("Hello, FreeCAD!".encode())

        connection.close()

receive_data()

Then click on the Run Script button (▶️ icon) in the header to run the script. You will notice that the Blender Window becomes unresponsive, but this is normal since the script is running on the main thread.

Now keep the terminal window (from which you launched Blender) next to the FreeCAD window side by side and run the say_hello() function in the Python Console.

You will notice that Data received: Hello, Blender! is printed in the Blender’s console. And similarly, Blender: Hello, FreeCAD! is printed in the FreeCAD Report View. Which means our socket connection is working successfully. 🙌🏻

Note: In order to make Blender UI responsive again, go to the terminal from which you launched Blender and press Ctrl + C. This will stop the execution of our script and will unblock the UI. In a later step, we will make the socket listens on a separate thread so that the UI doesn’t freeze.

2. Make the scripts load automatically on startup

Now that we have got the sockets part figured out, lets make out lives a little easier by making the scripts load automatically upon launching the application.

For FreeCAD, we will be create a dedicated menu named Blender with a single menu item inside called Export to Blender. At the moment, lets make it so that it every time you click on Blender > Export to Blender, it would run the say_hello() function. We would eventually make it run the export function. Create a file named BlenderLiveLink.py and paste the following code:

# Blender live link for FreeCAD

import socket
import FreeCAD as App
import FreeCADGui as Gui
from PySide2 import QtWidgets

def say_hello():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    client_socket.connect(server_address)

    client_socket.sendall("Hello, Blender!".encode())

    response = client_socket.recv(1024).decode()
    print("Blender:", response)

    client_socket.close()
    
def create_menu():
    menu = QtWidgets.QMenu("Blender")

    action = QtWidgets.QAction("Export to Blender", menu)
    action.triggered.connect(say_hello)

    menu.addAction(action)

    main_menu = Gui.getMainWindow().menuBar()
    main_menu.addMenu(menu)

Place this file in folder named BlenderLiveLink inside the Mod directory of the FreeCAD installation folder (refer to this article for your platform-specific location). This script packages the say_hello() function into a menu option. The create_menu() function creates a menu object named Blender using QMenu, then menu item named Export to Blender using QAction, assigns the say_hello() to be triggered every time it’s clicked and finally adds it to the FreeCAD menu bar.

Now, if you restart FreeCAD, you’ll notice that our menu doesn’t show up yet. This is because there is one more convention we need to follow to make FreeCAD detect our script on startup. We need an InitGui.py file alongside our script. Lets create that file in the same folder and paste the following code:

import FreeCADGui as Gui

def runStartupMacros(name):
    if name != "NoneWorkbench":
        Gui.getMainWindow().workbenchActivated.disconnect(runStartupMacros)
        import BlenderLiveLink
        BlenderLiveLink.create_menu()

import __main__
__main__.runStartupMacros = runStartupMacros

Gui.getMainWindow().workbenchActivated.connect(runStartupMacros)

This is directly coming from FreeCAD’s documentation. I initially tried putting all of the code in this file, but for some reason it didn’t work. Then I realized it’s because every function that you put in InitGui.py file, has to be explicitly added to __main__ like we are doing with the runStartupMacros function above. In the end, I chose to keep them as separate files, but feel free to put them in a single file and try it out.

After placing this in the same folder next to the BlenderLiveLink.py file, restart FreeCAD once more for the changes to reflect. This time you will notice that our Blender menu shows up. Feel free to test it by running our Blender script like we did in the previous step and clicking on Export to Blender. If everything is fine, you will notice our live links sends data to Blender every time you invoke Export to Blender.

Now, lets do the same thing with Blender by modifying our script to load on startup. Create a file named __init__.py and paste the following code into it:

import bpy
from bpy.app.handlers import persistent
import socket
import threading

bl_info = {
    "name": "FreeCAD Live Link",
    "description": "Live link for FreeCAD",
    "author": "Salai Vedha Viradhan",
    "version": (0, 1, 0),
    "blender": (2, 80, 0),
    "category": "Import-Export"
}

def receive_data():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for data...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()
        print(f"Data received: {data}")
        connection.sendall("Hello, FreeCAD!".encode())

        connection.close()

@persistent
def start_live_link(scene):
    threading.Thread(target=receive_data, args=()).start()

def register():
    bpy.app.handlers.load_post.append(start_live_link)

def unregister():
    bpy.app.handlers.load_post.remove(start_live_link)

The Blender side of things is going to get a little complex, particularly because we are treating it like a server application. A few things to note in the script above:

  • The bl_info is required when registering the script as an addon, and the Import-Export category will add to the respective section in the Add-ons tab.
  • Similarly, the register() and unregister() functions are required and is called by Blender when you activate and deactivate the addon respectively.
  • The bpy.app.handlers.load_post is a handler that tells Blender to do something once it loads a new Blender file. In this case it runs start_live_link() function on startup.
  • The interesting part is the start_live_link() function. In order to prevent the UI from freezing, we run the receive_data() function in a separate thread. And @persistent decorator is used to ensure the handler is run every time Blender is launched, because Blender cleans up all handlers when loading new files.

Place this file in the addons directory. Refer to this page in the Blender documentation for platform-specific locations.

With that in place, restart Blender, enable this addon from Preferences (Save Preferences if autosave is off) and restart Blender once more. Now, if you look at the terminal, you will see Listening for data… that we put in our script. Which means our socket created and listening for connections successfully.

Test if everything works fine by invoking Export to Blender from our FreeCAD menu. If everything went well, you will see the same hello messages being exchanged and printed in the consoles.

Now, there is one more thing we need to tackle on Blender’s side. If you try to quit Blender, you will see that it becomes unresponsive. This is because the socket is still open in the other thread even though Blender has killed the main thread. So we need to figure out a cleanup mechanism that can detect when the main thread has been killed and eventually close our socket running in the other thread, so that Blender can quit.

The threading module in Python has an enumerate() method that lets you iterate over all threads in the current interpreter. And all thread objects have a getName() and is_alive() methods that allow you to get the name of the thread and if the thread is still alive respectively. So, we can do something like this:

def cleanup_threads():
    threads_cleaned = False
    while not threads_cleaned:
        time.sleep(2)
        for thread in threading.enumerate():
            if thread.getName() == "MainThread" and thread.is_alive() == False:
                # Do something here to close the server socket
								
                threads_cleaned = True
                break

This function would run every two seconds until the main thread exits (don’t forget to import the time module at the top). For this function to actually close the socket connection after the main thread is killed, we will have to put some code in the if block before the break statement.

Currently, we are simply sending some data to Blender from FreeCAD and printing it to the console. We are not doing anything with the data itself. In order to close the socket when the main thread exits, we need to be able to tell the socket that the main thread has exit indeed in some way.

We can do this the same way we are sending data to Blender from FreeCAD. Just like how we are sending a hello message from FreeCAD, we can send some message from the cleanup_threads() function and modify our receive_data() function to read the data first and then decide what to do with it. Lets modify the cleanup_function() like so:

def cleanup_threads():
    threads_cleaned = False
    while not threads_cleaned:
        time.sleep(2)
        for thread in threading.enumerate():
            if thread.getName() == "MainThread" and thread.is_alive() == False:
                cleanup_socket = socket.socket()
                cleanup_socket.connect(('localhost', 25000))
                cleanup_socket.send(b"Quit Blender!")
                cleanup_socket.close()
                threads_cleaned = True
                break

When the main thread exits, the above function would connect to our server socket and send the message “Quit Blender!” to it. Now, we can modify our receive_data() function to handle this message and close the socket:

def receive_data():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for data...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()

        if data == "Quit Blender!":
            print("FreeCAD Live Link: Shutting down...")
            server_socket.close()
            break

        print(f"Data received: {data}")
        connection.sendall("Hello, FreeCAD!".encode())

        connection.close()

Here, one we establish a connection and receive some data, we basically check if it’s a message to quit blender. If yes, we close the server and break out of the loop, and thus the thread will exit automatically. We continue as usual if otherwise.

There is one last thing we need to do to make this work. We need to run this function on it’s own thread so it can keep checking the main thread. Just add one more line to the start_live_link() handler like so:

@persistent
def start_live_link(scene):
    threading.Thread(target=receive_data, args=()).start()
    threading.Thread(target=cleanup_threads, args=()).start()

Now launch Blender, try invoking Export to Blender in FreeCAD once and quit Blender. If everything went well, Blender should quit without any problems.

With that, we have most of the important moving parts in place. Now we can start worrying about the actual problem of exporting and importing the model.

3. Adding the export-import functionality

In FreeCAD, to export a model to OBJ, we can use the importOBJ module – sounds counterintuitive but that’s what we need. The importOBJ module has an export() method that takes two arguments: a list that holds all the objects that need to be exported and a file path where the models need to be exported.

So basically you can export a model from FreeCAD like this:

>>> import FreeCAD
>>> import importOBJ
>>> importOBJ.export(FreeCAD.ActiveDocument.Objects, "path/to/export.obj")

If you notice the arguments in the importOBJ.export() method, we specify the object list using the FreeCAD.ActiveDocument.Objects attribute. This would basically give us a list of all objects in the active document.

Similarly, to import an OBJ into Blender, we can use the bpy.ops.wm.obj_import() method (or bpy.ops.import_scene.obj() if you are running an older version that only has the legacy importer). It takes a filepath attribute that we can use to specify the path of the file that we just exported from FreeCAD using the API.

>>> bpy.ops.wm.obj_import(filepath="path/to/exported/file.obj")

One thing to note here is that Blender would allow only keyword arguments when using this method. So if you simply put the file path without the filepath= keyword, Blender would throw an error. Now if you run this code, you will notice that the imported model comes in the wrong orientation. Because FreeCAD exported it in a different orientation. In order to import with the right orientation, we need to specify two additional arguments:

>>> bpy.ops.wm.obj_import(filepath="path/to/exported/file.obj"), forward_axis='Y', up_axis='Z')

The forward_axis and up_axis explicitly tell the importer that the forward axis is Y and the up axis is Z respectively. Although we have the ability to define these axes, the OBJ imported doesn’t make any provision for us to specify the scale. Therefore the model will come in a default scale that is 100x bigger than the original model. But we can scale the imported objects down to their correct scale by doing:

>>> for obj in bpy.data.objects:  # loop through all objects in the file
...     if obj.select_get() == True:  # if the object is selected..
...         obj.scale = (0.01, 0.01, 0.01)  # ..set it's scale to 0.01
...
>>> bpy.ops.object.transform_apply(scale=True)  # and finally apply the scale

Since importing a model into Blender would automatically clear any existing selection and select the imported objects, we can use obj.select_get() to filter out the selected objects. Feel free to open FreeCAD and Blender and try out these snippets in their Python console.

The other thing to note is that, like I mentioned earlier, we could pass the forward_axis and up_axis data through our live link instead of hard-coding it in Blender. But for now, we are gonna leave it this way.

Now, lets add these snippets to our scripts. The FreeCAD side of things is relatively simple. You can just add the export code in place of the hello message like so:

import os
import socket
import importOBJ
import FreeCAD as App
import FreeCADGui as Gui
from PySide2 import QtWidgets
from tempfile import TemporaryDirectory

def export_obj():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    client_socket.connect(server_address)
    
    if App.ActiveDocument:
        temp_dir = TemporaryDirectory()
        obj_path = os.path.join(temp_dir.name, f"{App.ActiveDocument.Name}.obj")
        importOBJ.export(App.ActiveDocument.Objects, obj_path)

        client_socket.sendall(obj_path.encode())

        status_message = client_socket.recv(1024).decode()
        print("Blender:", status_message)

        temp_dir.cleanup()
    else:
        print('No model found to export. Please open a model file.')

    client_socket.close()
    

def create_menu():
    menu = QtWidgets.QMenu("Blender")

    action = QtWidgets.QAction("Export to Blender", menu)
    action.triggered.connect(export_obj)

    menu.addAction(action)

    main_menu = Gui.getMainWindow().menuBar()
    main_menu.addMenu(menu)

There are a few things worth noting here:

  • The import FreeCAD as App and import FreeCADGui as Gui are standard FreeCAD conventions for the respective modules and is how it internally references them.
  • The if App.ActiveDocument: checks if there’s any document open currently in FreeCAD. If yes, it proceeds with the export, or prints out an error message otherwise.
  • Instead of exporting the OBJ to some arbitrary location on the disk, we are exporting it to a temporary directory so that it can be cleaned up automatically once Blender has imported the OBJ. The tempfile.TemporaryDirectory() automatically creates a temp directory in the OS-specific temp folder.

You can feel free to restart FreeCAD to test this now. FreeCAD comes with a handful of sample files that you use. I used the ArchDetail.FCStd example in my case. Of course, Blender won’t be able to import the model yet, but you can see that Blender prints out the OBJ’s location in the temp folder.

With that in place, lets address the elephant in the room – importing the model into Blender.

First, lets put our import code in a separate function called import_obj() like so:

def import_obj():
    bpy.ops.wm.obj_import(filepath=obj_path, forward_axis='Y', up_axis='Z')

    for obj in bpy.data.objects:
        if obj.select_get() == True:
            obj.scale = (0.01, 0.01, 0.01)

    bpy.ops.object.transform_apply(scale=True)

Lets put it between the bl_info and the receive_data() function. Now, it might seem intuitive to call this function inside the receive_data() to import the model. Lets try that and see what happens:

def receive_data():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for OBJs...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()

        if data == "Quit Blender!":
            print("FreeCAD Live Link: Shutting down...")
            server_socket.close()
            break

        print(f"OBJ received: {data}")
        import_obj(data)
        connection.sendall("Successfully imported OBJ".encode())

        connection.close()

Put this in your Blender script and try invoking Export to Blender from FreeCAD. You will notice that Blender raises a RuntimeError saying that the context is incorrect:

Exception in thread Thread-1 (receive_data):
Traceback (most recent call last):
  File "/Applications/Blender 3.3.app/Contents/Resources/3.3/python/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
    self.run()
	  ...
  File "/Applications/Blender 3.3.app/Contents/Resources/3.3/scripts/modules/bpy/ops.py", line 113, in __call__
    ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Operator bpy.ops.wm.obj_import.poll() failed, context is incorrect

This is happening because Blender’s API is not thread-safe. And, when you are invoking the import_obj() inside the receive_data() function, it won’t have access to the right context since it’s running in a thread other than the main thread.

Now, the workarounds for this are quite complex and quirky. You can create a copy of the context and modify it temporarily inside the thread. This is known as context overriding. I believe you can also write a modal operator for this. But we are gonna take a different route to run this on the main thread. We are gonna write an OBJ monitor that watches a global variable and imports data whenever it becomes available.

First, lets move the obj_path from import_obj()’s parameter list and make it a global variable by placing it outside the function. And then lets write an obj_data_monitor() that will watch this variable for incoming data:

obj_path = None

def import_obj():
    global obj_path
    bpy.ops.wm.obj_import(filepath=obj_path, forward_axis='Y', up_axis='Z')

    for obj in bpy.data.objects:
        if obj.select_get() == True:
            obj.scale = (0.01, 0.01, 0.01)

    bpy.ops.object.transform_apply(scale=True)

def obj_data_monitor():
    global obj_path

    if obj_path != None:
        try:
            import_obj()
            obj_path = None       
        except Exception as e:
            print(str(e))
    return 1.0

If you notice, the obj_data_monitor() produces 1.0 as the return value. Blender has an interesting feature in the API called Application Timers. These timers basically allow you to schedule functions or run them repeatedly in a certain frequency. When a timer function returns a value, like we are doing above, it is telling the timer to run the function with that return value as the interval. So in our case, the function will run every second.

For this actually work, we need to register it using the bpy.app.timers.register() method. Lets modify our start_live_link() function like so:

@persistent
def start_live_link(scene):
    threading.Thread(target=receive_data, args=()).start()
    threading.Thread(target=cleanup_threads, args=()).start()
    bpy.app.timers.register(obj_data_monitor)

Now, for the monitor to work, we need to do something in our receive_data() function. Because currently the obj_path is always going to be None. Lets modify the receive_data() function like so:

def receive_data():
    global obj_path
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for OBJs...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()

        if data == "Quit Blender!":
            print("FreeCAD Live Link: Shutting down...")
            server_socket.close()
            break

        print(f"OBJ received: {data}")
        obj_path = data
        connection.sendall("Successfully imported OBJ".encode())

        connection.close()

Two things to note:

  • We have replaced import_obj(data) with obj_path = data after the last print statement, and modified the message inside connection.sendall().
  • Declared global obj_path at the top of the function for it to use our global variable. In some cases this is not required, but it’s better to be explicit.

If you invoke Export to Blender from FreeCAD now, you will see that FreeCAD prints out a success message that we are sending back, but the model still doesn’t show up in Blender.

This is happening because, once we assign the OBJ path to our global variable by doing obj_path = data, we are immediately sending a success message to FreeCAD. If you look at the FreeCAD code, you will notice that the moment it hears back from Blender, the next step is to cleanup the temporary directory using temp_dir.cleanup(). But Blender would not have imported the model yet, and since the OBJ is deleted when the temp directory is cleaned, Blender doesn’t get its hand on the model at all.

In order to wait until Blender has imported the OBJ before sending a success message to FreeCAD, we have introduce another status variable. Lets call this variable import_status and it would assume three states:

  • IMPORTING – set when Blender is importing a model. This will allow us to hold the success message until the import completes.
  • SUCCESS and FAILURE – set when the import succeeds or fails respectively.

Lets add this next to our global obj_path variable and modify the obj_data_monitor() and receive_data() functions like so:

obj_path = None
import_status = None

...

def obj_data_monitor():
    global obj_path
    global import_status

    if obj_path != None:
        try:
            import_obj()
            obj_path = None
            import_status = 'SUCCESS'    
        except Exception as e:
            print(str(e))
            import_status = 'FAILURE'    
    return 1.0

def receive_data():
    global obj_path
    global import_status

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('localhost', 25000)
    server_socket.bind(server_address)

    while True:
        server_socket.listen(5)
        print("Listening for OBJs...")

        connection, client_address = server_socket.accept()
        print("Connected with FreeCAD instance:", client_address)

        data = connection.recv(1024).decode()

        if data == "Quit Blender!":
            print("FreeCAD Live Link: Shutting down...")
            server_socket.close()
            break

        print(f"OBJ received: {data}")
        import_status = 'IMPORTING'
        obj_path = data

        while import_status != None:
            if import_status == 'SUCCESS':
                connection.sendall("Successfully imported OBJ!".encode())
                break
            elif import_status == 'FAILURE':
                connection.sendall("Failed imported OBJ.".encode())
                break
            else:
                time.sleep(3)
                continue

        import_status = None
        connection.close()

In the above snippet you can notice that:

  • Both obj_data_monitor() and receive_data() use the global import_status variable at the top.
  • The receive_data() function sets the status to IMPORTING before assigning data to obj_path. This would allow the following loop to run until the import succeeds or fails.
  • The obj_data_monitor() function sets the status to SUCCESS or FAILURE depending on whether the import succeeded or failed respectively. Which is then read by the loop in receive_data() to send back a respective message to FreeCAD.

And, that’s it! There you have it. You very own live link for Blender.

Restart Blender, open ArchDetail.FCStd sample in FreeCAD and invoke Export to Blender and watch the model coming into Blender!

Closing thoughts

I know that was a really long post, but I hope you enjoyed it. 😅 

Although there a lot of quirks around building live links, you can see that it’s not that difficult to build one yourself. Once you understand the ideas laid out in this post, you can build live links for any applications that have an API and allow you to create socket connections.

We have just scratched the surface. Obviously there is a lot of room improvements and exploration. You maybe pondering over questions like:

  • What if multiple Blender instances are running? How would FreeCAD know which instance to send the model to?
  • What if I exported the model to Blender already and then made a change in FreeCAD? Can I update only the corresponding portion of the model?

These are wonderful ideas to explore and build upon. Also, none of this is limited to applications that have a Python API. Sockets are platform and language agnostic. For example you could build a live link for SketchUp and Blender using SketchUp’s Ruby API. Or from Roblox Studio using the Lua API.

The possibilities are endless. I leave it to you now to come up with more cool stuff. 😃

That’s it for now! 🙌🏻

Let me know what you think on Twitter or LinkedIn.

P.S

You can get the final source code and a PDF version of the article (if you prefer) on Gumroad.

Resources