Using Solid Node

Make sure you have completed the :Quickstart. At this point, you should be able to view your project at the viewer - either Openscad or the web viewer - and have a source code to edit.

In Solid Node, project is organized in a tree structure, with leaf nodes and internal nodes. Leaf nodes use uderlying modelling libraries, like SolidPython and CadQuery, to generate solid models. Internal Nodes combine children nodes in some way, like an Assembly or Fusion

Each node implements the render() method. Leaf nodes return an object of the underlying library. Internal nodes render() should return a list of child instances.

Leaf Nodes

Solid2Node

The initial example in solid-seed implements a Solid2Node node, which uses solidpython2 to create models. Open root/__init__.py:

from solid_node.node import Solid2Node
from solid2 import cube, cylinder, translate

class DemoProject(Solid2Node):

    def render(self):
        return translate(-25, -25, 0)(
            cube(50, 50, 50)
        ) - cylinder(r=10, h=100)

CadQueryNode

The same model can be obtained using CadQuery:

import cadquery as cq
from solid_node.node import CadQueryNode

class DemoProject(CadQueryNode):

    def render(self):
        wp = cq.Workplane("XY")
        cube = wp.box(50, 50, 50)
        hole = wp.workplane(offset=-50).circle(10).extrude(100)
        return cube.cut(hole)

Internal Nodes

There are two types of internal nodes: AssemblyNode and FusionNode. An AssemblyNode is an assemble of its children nodes, while in FusionNode the children nodes are fused in one mesh.

Simple Clock Example

Let’s make a very simple clock, as a proof of concept, mixing together CadQuery and SolidPython, so we can demonstrate use of time and testing.

Create a new file root/clock_base.py and create a CadQueryNode:

import cadquery as cq
from solid_node.node import CadQueryNode

class ClockBase(CadQueryNode):

    def render(self):
        wp = cq.Workplane("XY")
        return wp.circle(100).extrude(2)

Now, a file root/pointer.py with a Solid2Node:

from solid_node.node import Solid2Node
from solid2 import cube, cylinder, translate

class Pointer(Solid2Node):

    def render(self):
        return translate(-5, -5, 3)(
            cube(10, 90, 10)
        )

And at root/__init__.py, an AssemblyNode

from solid_node.node import AssemblyNode
from .clock_base import ClockBase
from .pointer import Pointer

class SimpleClock(AssemblyNode):

    base = ClockBase()
    pointer = Pointer()

    def render(self):
        return [self.base, self.pointer]

Now in the viewer you should see a round clock base with a pointer.

Using time

The AssemblyNode can use the property self.time to position elements. The time is a number between 0 and 1 that will be resolved in the viewer, and you can use it to position elements relative to time.

Edit root/__init__.py to rotate the pointer:

class SimpleClock(AssemblyNode):

    base = ClockBase()
    pointer = Pointer()

    def render(self):
        angle = 360 * self.time
        self.pointer.rotate(angle, [0, 0, 1])
        return [self.base, self.pointer]

At this point you should see a rotating pointer in the viewer. If you are using the Openscad viewer, you need to enable animation (View -> Animate) and set fps and number of frames. Reload is not automatic in Openscad while animating.

Testing

Solid Node has a test runner and solid_node.test.TestCase extension to run tests with meshes. As an example, you could use, for example. AssertNotIntersecting to verify that two gears do not overlap during movement, or AssertIntersecting to verify that a handle is not detached during movement.

There is also solid_node.test.TestCaseMixin, which allows you to write tests in your node class instead of using a separate file.

To demonstrate testing, let’s make a pin holding the pointer and base together. First, to create a hole at the base, edit root/clock_base.py

class ClockBase(CadQueryNode):

    def render(self):
        wp = cq.Workplane("XY")
        return wp.circle(100).extrude(2)

And a hole in the pointer, at root/pointer.py

class Pointer(Solid2Node):

    def render(self):
        pointer = translate(-5, -5, 3)(
            cube(10, 90, 10)
        )
        hole = cylinder(r=3, h=15)
        return pointer - hole

Now, you should see a hole through both pointer and pin, while the pointer is rotating.

Let’s make a pin through them. Create the file root/pin.py:

from solid_node.node import Solid2Node
from solid2 import cube, cylinder, translate

class Pin(Solid2Node):

    def render(self):
        return cylinder(r=3, h=20)

And at root/__init__.py, assemble the pin together:

from solid_node.node import AssemblyNode
from .clock_base import ClockBase
from .pointer import Pointer
from .pin import Pin

class SimpleClock(AssemblyNode):

    base = ClockBase()
    pointer = Pointer()
    pin = Pin()

    def render(self):
        angle = 360 * self.time
        self.pointer.rotate(angle, [0, 0, 1])
        return [self.base, self.pointer, self.pin]

You should see the pin rendered in viewer, with a tight fit. We want to test if this is functional: if in reality, this arrangement will work. So, let’s write a test.

For that, we’ll use solid_node.test.TestCaseMixin. Add it to the base classes of the root node at root/__init__.py:

...
from solid_node.test import TestCaseMixin

class SimpleClock(AssemblyNode, TestCaseMixin)

Now we’ll add tests to our root node. Our SimpleClock class will extend solid_node.test.TestCaseMixin and we’ll add two tests to root/__init__.py:

from solid_node.node import AssemblyNode
from solid_node.test import TestCaseMixin
from .clock_base import ClockBase
from .pointer import Pointer
from .pin import Pin

class SimpleClock(AssemblyNode, TestCaseMixin):

    base = ClockBase()
    pointer = Pointer()
    pin = Pin()

    def render(self):
        ...

    def test_pin_runs_free_in_base(self):
        self.assertNotIntersecting(self.base, self.pin)

    def test_pin_runs_free_in_pointer(self):
        self.assertNotIntersecting(self.pointer, self.pin)

On the command line, stop the solid root develop command, and run solid root test.

You should see two tests failing, as in practice there is a very small intersection between rendered meshes even though matematically they should not. Let’s reduce the radius of our pin to 2.9, at root/pin.py:

class Pin(Solid2Node):

    def render(self):
        return cylinder(r=2.9, h=20)

Run the tests again. This time, the two tests will pass. If you look closely, the cylinder of the pins are not really round. They are an approximation. This is because internally STLs are generated for the models.

The tests are passing, the pieces are not intersecting. But would they still not intersect during the rotation of the pointer? The test we made just tested the situation for the initial setup. We can improve the test by using the decorator solid_node.test.testing_steps:

   ...
   from solid_node.test import TestCaseMixin, testing_steps

   class SimpleClock(AssemblyNode, TestCaseMixin):
       ...

       @testing_steps(3, end=0.1)
       def test_pin_runs_free_in_base(self):
           self.assertNotIntersecting(self.base, self.pin)

       @testing_steps(3, end=0.1)
       def test_pin_runs_free_in_pointer(self):
           self.assertNotIntersecting(self.pointer, self.pin)

The tests above will run three times, at three different instants,
from time 0 to 0.1.