FreeCAD Jupyter translation

First we load required libraries for the translation from FreeCAD Coin3D scene graph to a pythreejs WebGL rendering scene. And even before that make sure you have a working FreeCAD and python3 setup. Then install pythreejs:

pip3 install pythreejs
jupyter nbextension install --py --symlink --sys-prefix pythreejs
jupyter nbextension enable --py --sys-prefix pythreejs

The following function will do the job: freecadviewer.render_document is everything that you need to know in order to render your FreeCAD document.

For this to work we need to iterate over the entire scene graph and extract the edges and surfaces. By default we grab the face and edge representations (by chosing the right switch node children. The switch node allows switching between different FreeCAD views such as showing the mesh, surface, only edges etc.)

To view the scene graph structure in a convenient way open FreeCAD > Tools > view scene graph in the GUI. That's how I verified the locations of SoIndexedFaceSet and the corresponding SoCoordinate3 object. The coordinates switch node children refer to are always the coordinates in the root of the object. So one level below the document root node.

Importing FreeCAD

Now we can verify that these functions do what they are supposed to do. First we add the JUPYTER_REPO_PATH (path to the Github repository this file is part of) and the FreeCAD shared library path. Then we just import FreeCAD and set it up for headless usage. No firing up the desktop app!

In [1]:
import sys, os

JUPYTER_REPO_PATH = "/opt/jupyter_freecad/"

sys.path.append("/opt/freecad/freecad_build/lib")
sys.path.append(JUPYTER_REPO_PATH + "IPythonFreeCADViewer")

import FreeCAD, FreeCADGui
from freecadviewer import render_document, get_document_renderer

FreeCADGui.setupWithoutGUI()

Creating a document with objects and a scene graph to be iterated over later on.

In [2]:
doc = FreeCAD.newDocument("test_document")
doc.addObject("Part::Box","Box")
doc.addObject("Part::Cylinder","Cylinder")
doc.addObject("Part::Sphere","Sphere")
doc.addObject("Part::Torus","Torus")
doc.recompute()
doc.FileName = "test_document.FCStd"
doc.save()

Now if everything works as expected this is all we need to render the 3D view right in the notebook:

In [3]:
render_document(doc)

Renderer settings

There are a few options to modify what is being rendered, you can enable or disable the rendering of faces, edges, normals and face meshes. We can also check what the default config is:

In [4]:
from freecadviewer import RendererConfig

renderer_config = RendererConfig()
renderer_config.show_config()
{'show_mesh': False, 'show_edges': True, 'show_faces': True, 'show_normals': False, 'view_width': 600, 'view_height': 600, 'selection_mode': 'mousemove'}

Now we can modify these settings to our wishes:

In [5]:
renderer_config.selection_mode = None
renderer_config.show_edges = False
renderer_config.view_width = renderer_config.view_height = 400
render_document(doc, renderer_config)
In [6]:
renderer_config.show_mesh = True
renderer_config.show_normals = True
render_document(doc, renderer_config)
In [7]:
renderer_config.show_edges = True
renderer_config.show_normals = False
renderer_config.show_faces = False
renderer_config.selection_mode = "mousemove"
render_document(doc, renderer_config)

Taking advantage of Ipython plugins

ipywidgets example

The following code will display an interactive view that will modify FreeCAD document content based on interacive user input via the ipywidget UI elements.

In [8]:
from ipywidgets import interact
import ipywidgets as widgets

doc_dyn = FreeCAD.newDocument()
doc_dyn.addObject("Part::Box","Box")

renderer_config_ = RendererConfig()
renderer_config_.view_width = renderer_config_.view_height = 400

def create_scene(box, cylinder, sphere, torus):
    if box:
        if not doc_dyn.getObject("Box"):
            doc_dyn.addObject("Part::Box","Box")
    elif doc_dyn.getObject("Box"):
        doc_dyn.removeObject("Box")
    if cylinder:
        if not doc_dyn.getObject("Cylinder"):
            doc_dyn.addObject("Part::Cylinder","Cylinder")
    elif doc_dyn.getObject("Cylinder"):
        doc_dyn.removeObject("Cylinder")
    if sphere:
        if not doc_dyn.getObject("Sphere"):
            doc_dyn.addObject("Part::Sphere","Sphere")
    elif doc_dyn.getObject("Sphere"):
        doc_dyn.removeObject("Sphere")
    if torus:
        if not doc_dyn.getObject("Torus"):
            doc_dyn.addObject("Part::Torus","Torus")
    elif doc_dyn.getObject("Torus"):
        doc_dyn.removeObject("Torus")
    doc_dyn.recompute()

    return render_document(doc_dyn, renderer_config_)

interact(create_scene, box=True, cylinder=False, sphere=False, torus=False);

You can also recreate the same thing visuall by just linking the buttons and the view without going through the Python kernel by using jslink. That way the visual change is faster and you could embed this interactive view on a static page like here. You can do this for the widget above as follows:

In [9]:
from ipywidgets import jslink, Checkbox

renderer_config_.show_edges = False
# workaround since it becomes hard to identify the objects in
# the scene group since all lines and meshes are in the
# same group. Fixing this is a TODO

doc_stat = FreeCAD.newDocument()
doc_stat.addObject("Part::Box","Box")
doc_stat.addObject("Part::Cylinder","Cylinder")
doc_stat.addObject("Part::Sphere","Sphere")
doc_stat.addObject("Part::Torus","Torus")
doc_stat.recompute()

renderer, html = get_document_renderer(doc_stat, renderer_config_)
checkboxes = [Checkbox(description="Box"), Checkbox(description="Cylinder"), Checkbox(description="Sphere"), Checkbox(description="Torus"), ]
for i, obj in enumerate(renderer.scene.children[3].children):
    jslink((checkboxes[i], "value"), (obj, "visible"))

checkboxes[0].value = True
checkboxes.extend([renderer, html])
display(*checkboxes)

Opening the previously saved document

In [10]:
doc_prev = FreeCAD.openDocument(JUPYTER_REPO_PATH + "test_document.FCStd")
render_document(doc_prev, renderer_config_)

Opening a file created with the FreeCAD dektop app

Created with FreeCAD 0.18 on MacOS

In [11]:
doc_desktop = FreeCAD.openDocument(JUPYTER_REPO_PATH + "test_freecad_0.18_macos.FCStd")
render_document(doc_desktop, renderer_config_)