Skip to content

Commit b5d44b4

Browse files
committed
docs
1 parent bbfad8e commit b5d44b4

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

docs/using-solid-node.rst

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
2+
.. _using-solid-node:
3+
4+
================
5+
Using Solid Node
6+
================
7+
8+
Make sure you have completed the ::doc:`Quickstart <quickstart>`.
9+
At this point, you should be able to view your project at the viewer
10+
- either Openscad or the web viewer - and have a source code to edit.
11+
12+
Getting started
13+
---------------
14+
15+
Open `root/__init__.py`, you should have the code for a cube with a hole
16+
in it:
17+
18+
```python
19+
from solid_node.node import Solid2Node
20+
from solid2 import cube, cylinder, translate
21+
22+
class DemoProject(Solid2Node):
23+
24+
def render(self):
25+
return translate(-25, -25, 0)(
26+
cube(50, 50, 50)
27+
) - cylinder(r=10, h=100)
28+
```
29+
30+
In Solid Node, project is organized in a tree structure, with leaf nodes
31+
and internal nodes. Leaf nodes use uderlying modelling libraries, like
32+
SolidPython and CadQuery, to generate solid models.
33+
34+
The starting example has just one Solid2Node node, which is a leaf node
35+
using SolidPython.
36+
The same result can be obtained using a CadQueryNode:
37+
38+
```python
39+
import cadquery as cq
40+
from solid_node.node import CadQueryNode
41+
42+
class DemoProject(CadQueryNode):
43+
44+
def render(self):
45+
wp = cq.Workplane("XY")
46+
cube = wp.box(50, 50, 50)
47+
hole = wp.workplane(offset=-50).circle(10).extrude(100)
48+
return cube.cut(hole)
49+
```
50+
51+
Each node must implement the `render()` method. Leaf nodes should return
52+
an object of the underlying library. Internal nodes `render()` should return
53+
a list of child instances.
54+
55+
There are two types of internal nodes: `AssemblyNode` and `FusionNode`.
56+
An AssemblyNode is an assemble of its children nodes, while in FusionNode
57+
the children nodes are fused in one mesh.
58+
59+
Let's make a very simple clock, as a proof of concept, mixing together
60+
CadQuery and SolidPython
61+
62+
Simple Clock
63+
------------
64+
65+
Create a new file `root/clock_base.py` and create a `CadQueryNode`:
66+
67+
```python
68+
import cadquery as cq
69+
from solid_node.node import CadQueryNode
70+
71+
class ClockBase(CadQueryNode):
72+
73+
def render(self):
74+
wp = cq.Workplane("XY")
75+
return wp.circle(100).extrude(2)
76+
```
77+
78+
Now, a file `root/pointer.py` with a `Solid2Node`:
79+
80+
```python
81+
from solid_node.node import Solid2Node
82+
from solid2 import cube, cylinder, translate
83+
84+
class Pointer(Solid2Node):
85+
86+
def render(self):
87+
return translate(-5, -5, 3)(
88+
cube(10, 90, 10)
89+
)
90+
```
91+
92+
And at `root/__init__.py`, an `AssemblyNode`
93+
94+
```python
95+
from solid_node.node import AssemblyNode
96+
from .clock_base import ClockBase
97+
from .pointer import Pointer
98+
99+
class SimpleClock(AssemblyNode):
100+
101+
base = ClockBase()
102+
pointer = Pointer()
103+
104+
def render(self):
105+
return [self.base, self.pointer]
106+
```
107+
108+
Now in the viewer you should see a round clock base with a pointer.
109+
The `AssemblyNode` can use the property `self.time` to position elements.
110+
111+
Edit `root/__init__.py` to rotate the pointer:
112+
113+
```python
114+
from solid_node.node import AssemblyNode
115+
from .clock_base import ClockBase
116+
from .pointer import Pointer
117+
118+
class SimpleClock(AssemblyNode):
119+
120+
base = ClockBase()
121+
pointer = Pointer()
122+
123+
def render(self):
124+
angle = 360 * self.time
125+
self.pointer.rotate(angle, [0, 0, 1])
126+
return [self.base, self.pointer]
127+
```
128+
129+
At this point you should see a rotating pointer in the viewer.
130+
If you are using the Openscad viewer, you need to enable animation
131+
(View -> Animate) and set fps and number of frames.
132+
Reload is not automatic in Openscad while animating.
133+
134+
Let's make a pin holding the pointer and base together.
135+
First, to create a hole at the base, edit `root/clock_base.py`
136+
137+
```python
138+
class ClockBase(CadQueryNode):
139+
140+
def render(self):
141+
wp = cq.Workplane("XY")
142+
return wp.circle(100).extrude(2)
143+
```
144+
145+
And a hole in the pointer, at `root/pointer.py`
146+
147+
```python
148+
class Pointer(Solid2Node):
149+
150+
def render(self):
151+
pointer = translate(-5, -5, 3)(
152+
cube(10, 90, 10)
153+
)
154+
hole = cylinder(r=3, h=15)
155+
return pointer - hole
156+
```
157+
158+
Now, you should see a hole through both pointer and
159+
pin, while the pointer is rotating.
160+
161+
Let's make a pin through them. Create the file `root/pin.py`:
162+
163+
```python
164+
from solid_node.node import Solid2Node
165+
from solid2 import cube, cylinder, translate
166+
167+
class Pin(Solid2Node):
168+
169+
def render(self):
170+
return cylinder(r=3, h=20)
171+
```
172+
173+
And at `root/__init__.py`, assemble the pin together:
174+
175+
```python
176+
from solid_node.node import AssemblyNode
177+
from .clock_base import ClockBase
178+
from .pointer import Pointer
179+
from .pin import Pin
180+
181+
class SimpleClock(AssemblyNode):
182+
183+
base = ClockBase()
184+
pointer = Pointer()
185+
pin = Pin()
186+
187+
def render(self):
188+
angle = 360 * self.time
189+
self.pointer.rotate(angle, [0, 0, 1])
190+
return [self.base, self.pointer, self.pin]
191+
```
192+
193+
You should see the pin rendered in viewer, with a tight fit.
194+
We want to test if this is functional: if in reality, this
195+
arrangement will work. So, let's write a test.
196+
197+
For that, we'll use `solid_node.test.TestCaseMixin`. Add
198+
it to the base classes of the root node at `root/__init__.py`:
199+
200+
```python
201+
...
202+
from solid_node.test import TestCaseMixin
203+
204+
class SimpleClock(AssemblyNode, TestCaseMixin)
205+
```
206+
207+
Now we'll add tests to our root node. Our SimpleClock
208+
class will extend `solid_node.test.TestCaseMixin` and
209+
we'll add two tests to `root/__init__.py`:
210+
211+
```python
212+
from solid_node.node import AssemblyNode
213+
from solid_node.test import TestCaseMixin
214+
from .clock_base import ClockBase
215+
from .pointer import Pointer
216+
from .pin import Pin
217+
218+
class SimpleClock(AssemblyNode, TestCaseMixin):
219+
220+
base = ClockBase()
221+
pointer = Pointer()
222+
pin = Pin()
223+
224+
def render(self):
225+
...
226+
227+
def test_pin_runs_free_in_base(self):
228+
self.assertNotIntersecting(self.base, self.pin)
229+
230+
def test_pin_runs_free_in_pointer(self):
231+
self.assertNotIntersecting(self.pointer, self.pin)
232+
```
233+
234+
On the command line, stop the `solid root develop` command, and
235+
run `solid root test`.
236+
237+
You should see two tests failing, as in practice there is a very
238+
small intersection between rendered meshes even though matematically
239+
they should not. Let's reduce the radius of our pin to 2.9, at
240+
`root/pin.py`:
241+
242+
```python
243+
class Pin(Solid2Node):
244+
245+
def render(self):
246+
return cylinder(r=2.9, h=20)
247+
```
248+
249+
Run the tests again. This time, the two tests will pass.
250+
If you look closely, the cylinder of the pins are not really round.
251+
They are an approximation. This is because internally STLs are generated
252+
for the models.
253+
254+
The tests are passing, the pieces are not intersect. But would they still
255+
not intersect during the rotation of the pointer? The test we made just
256+
tested the situation for the initial setup. We can improve the test
257+
by using the decorator `solid_node.test.testing_steps`:
258+
259+
```python
260+
...
261+
from solid_node.test import TestCaseMixin, testing_steps
262+
263+
class SimpleClock(AssemblyNode, TestCaseMixin):
264+
...
265+
266+
@testing_steps(3, end=0.1)
267+
def test_pin_runs_free_in_base(self):
268+
self.assertNotIntersecting(self.base, self.pin)
269+
270+
def test_pin_runs_free_in_pointer(self):
271+
self.assertNotIntersecting(self.pointer, self.pin)
272+
```
273+
274+
275+
276+

0 commit comments

Comments
 (0)