Skip to content

Commit fb1b169

Browse files
authored
Merge pull request #761 from compas-dev/robot_tutorial_update
Robot tutorial update
2 parents 4402fcb + 471c104 commit fb1b169

File tree

13 files changed

+337
-87
lines changed

13 files changed

+337
-87
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
* Fixed bug in parameter list of function `mesh_bounding_box` bound as method `Mesh.bounding_box`.
1717
* Changed exception type when subdivide scheme argument is incorrect on `mesh_subdivide`.
1818
* The `compas_rhino.artist.RobotModelArtist` functions `draw_visual` and `draw_collision` now return list of newly created Rhino object guids.
19+
* Added ability of `RobotModel.add_link` to accept primitives in addition to meshes.
20+
* Fixed bug regarding the computation of `Joint.current_origin`.
21+
* Fixed bug regarding a repeated call to `RobotModel.add_joint`.
1922

2023
### Removed
2124

docs/tutorial/files/boxy_1.png

91.9 KB
Loading

docs/tutorial/files/boxy_2.png

99.4 KB
Loading

docs/tutorial/files/boxy_3.png

167 KB
Loading
218 KB
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from compas.robots import Joint
2+
from compas.robots import RobotModel
3+
from compas.geometry import Box
4+
from compas.geometry import Circle
5+
from compas.geometry import Cylinder
6+
from compas.geometry import Frame
7+
from compas.geometry import Plane
8+
from compas.geometry import Sphere
9+
from compas.geometry import Vector
10+
from compas_rhino.artists import RobotModelArtist
11+
12+
model = RobotModel('drinking_bird')
13+
14+
foot_1 = Box(Frame([2, .5, .25], [1, 0, 0], [0, 1, 0]), 1, 2, .5)
15+
foot_2 = Box(Frame([-2, .5, .25], [1, 0, 0], [0, 1, 0]), 1, 2, .5)
16+
leg_1 = Box(Frame([2, 0, 4], [1, 0, 0], [0, 1, 0]), .5, 1, 7)
17+
leg_2 = Box(Frame([-2, 0, 4], [1, 0, 0], [0, 1, 0]), .5, 1, 7)
18+
axis = Cylinder(Circle(Plane([0, 0, 7], [1, 0, 0]), .01), 4)
19+
legs_link = model.add_link('legs', visual_meshes=[foot_1, foot_2, leg_1, leg_2, axis])
20+
21+
torso = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), .5), 8)
22+
torso_link = model.add_link('torso', visual_meshes=[torso])
23+
24+
legs_joint_origin = Frame([0, 0, 7], [1, 0, 0], [0, 1, 0])
25+
joint_axis = Vector(1, 0, 0)
26+
model.add_joint(
27+
'torso_base_joint',
28+
Joint.CONTINUOUS,
29+
legs_link,
30+
torso_link,
31+
legs_joint_origin,
32+
joint_axis
33+
)
34+
35+
head = Sphere([0, 0, 0], 1)
36+
beak = Cylinder(Circle(Plane([0, 1, -.3], [0, 1, 0]), .3), 1.5)
37+
head_link = model.add_link('head', visual_meshes=[head, beak])
38+
neck_joint_origin = Frame([0, 0, 4], [1, 0, 0], [0, 1, 0])
39+
model.add_joint('neck_joint', Joint.FIXED, torso_link, head_link, origin=neck_joint_origin)
40+
41+
tail = Sphere([0, 0, 0], 1)
42+
tail_link = model.add_link('tail', visual_meshes=[tail])
43+
tail_joint_origin = Frame([0, 0, -4], [1, 0, 0], [0, 1, 0])
44+
model.add_joint('tail_joint', Joint.FIXED, torso_link, tail_link, origin=tail_joint_origin)
45+
46+
hat = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), .8), 1.5)
47+
brim = Cylinder(Circle(Plane([0, 0, -1.5/2], [0, 0, 1]), 1.4), .1)
48+
hat_link = model.add_link('hat', visual_meshes=[hat, brim])
49+
hat_joint_origin = Frame([0, 0, 1 - .3 + 1.5/2], [1, 0, 0], [0, 1, 0])
50+
model.add_joint('hat_joint', Joint.FIXED, head_link, hat_link, origin=hat_joint_origin)
51+
52+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
53+
artist.clear_layer()
54+
artist.draw_visual()

docs/tutorial/files/jointy.png

198 KB
Loading

docs/tutorial/robots.rst

Lines changed: 186 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,36 @@ of joints:
141141
* **Fixed**: Not really a joint because it cannot move, all degrees of freedom
142142
are locked.
143143

144+
145+
Visualizing Robots
146+
==================
147+
148+
Before jumping into how to build a robot model, let's first see how to visualize
149+
one. This can be done with Blender, Rhino or Grasshopper using one of COMPAS's
150+
artists. The basic procedure is the same in
151+
any of the CAD software (aside from the import statement), so for simplicity we
152+
will demonstrate the use of :class:`compas_rhino.artists.RobotModelArtist` in Rhino.
153+
Be sure to first install COMPAS for Rhino. While the following code is incomplete,
154+
it can be used as a scaffolding for code to be run in a Python script editor within Rhino.
155+
156+
.. code-block:: python
157+
158+
import compas
159+
from compas.robots import RobotModel
160+
from compas_rhino.artists import RobotModelArtist
161+
162+
model = RobotModel('Robby')
163+
164+
# Add some geometry to Robby here
165+
166+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
167+
artist.clear_layer()
168+
artist.draw_visual()
169+
170+
171+
See below for a complete example.
172+
173+
144174
Building robots models
145175
======================
146176

@@ -175,35 +205,170 @@ shows how to construct one programmatically:
175205

176206
This approach can end up being very verbose, so the methods ``add_link``
177207
and ``add_joint`` of :class:`compas.robots.RobotModel` offer an alternative that
178-
significantly reduces the amount of code required:
208+
significantly reduces the amount of code required. Starting with an empty
209+
robot model, adding a link in the shape of a box is as easy as:
179210

180211
::
181212

182213
>>> from compas.geometry import Box, Frame
183-
>>> from compas.datastructures import Mesh
184-
>>> from compas.robots import Joint, Link, RobotModel
214+
>>> from compas.robots import RobotModel
185215

186216
::
187217

188-
>>> length, width, axis = 5, 0.4, (0, 0, 1)
189-
>>> box = Box.from_diagonal([(0, width / -2, width / -2), (length, width / 2, width / 2)])
190-
>>> robot = RobotModel('bender')
191-
>>> link_last = robot.add_link('world')
192-
>>> robot.name
193-
'bender'
194-
195-
>>> for i in range(6):
196-
... visual_mesh = Mesh.from_shape(box)
197-
... origin = Frame.from_quaternion((1, 0, 0, 0), point=(length, 0, 0))
198-
... link = robot.add_link('link_{}'.format(i), visual_mesh)
199-
... robot.add_joint('joint_{}'.format(i), Joint.CONTINUOUS, link_last, link, origin, axis)
200-
... link_last = link
201-
...
202-
>>> robot.get_configurable_joint_names()
203-
['joint_0', 'joint_1', 'joint_2', 'joint_3', 'joint_4', 'joint_5']
218+
>>> model = RobotModel(name='Boxy')
219+
>>> box = Box(Frame.worldXY(), 1, 2, .5)
220+
>>> model.add_link(name='box_link', visual_meshes=[box])
221+
222+
This code snippet can be modified and run in a Rhino python editor
223+
to visualize Boxy. Throughout the rest of this tutorial, the code
224+
snippets will include the lines for visualization in Rhino, but be
225+
aware that the class :class:`compas.robots.RobotModel` can be used,
226+
and is useful, outside of a CAD environment.
227+
228+
.. code-block:: python
229+
230+
from compas.geometry import Box, Frame
231+
from compas.robots import RobotModel
232+
from compas_rhino.artists import RobotModelArtist
233+
234+
model = RobotModel(name='Boxy')
235+
box = Box(Frame.worldXY(), 1, 2, .5)
236+
model.add_link(name='box_link', visual_meshes=[box])
237+
238+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
239+
artist.clear_layer()
240+
artist.draw_visual()
241+
242+
243+
.. figure:: files/boxy_1.png
244+
:figclass: figure
245+
:class: figure-img img-fluid
246+
247+
As can be seen, this has added a box of dimensions ``1`` x ``2`` x ``.5``
248+
whose geometric center and orientation coincides with the world XY frame.
249+
The ``visual_meshes`` argument can be given a list containing COMPAS
250+
primitives such as :class:`compas.geometry.Box` or the more complex
251+
COMPAS meshes :class:`compas.geometry.Mesh`. For simplicity,
252+
this tutorial uses only primitives.
253+
254+
To reposition the box relative to the link's
255+
origin, simply change the frame of the provided box. To move the box so that it sits
256+
above the XY plane, the origin must be shifted in the z-direction by half the height
257+
of the box. The box is also shifted slightly forward in the y-direction:
258+
259+
.. code-block:: python
260+
261+
from compas.geometry import Box, Frame
262+
from compas.robots import RobotModel
263+
from compas_rhino.artists import RobotModelArtist
264+
265+
model = RobotModel(name='Boxy')
266+
box = Box(Frame([0, .5, .25], [1, 0, 0], [0, 1, 0]), 1, 2, .5)
267+
model.add_link(name='box_link', visual_meshes=[box])
268+
269+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
270+
artist.clear_layer()
271+
artist.draw_visual()
204272
205-
However, more often than not, robot models are loaded from URDF files instead of being
206-
defined programmatically. To load a URDF into a robot model, use the
273+
.. figure:: files/boxy_2.png
274+
:figclass: figure
275+
:class: figure-img img-fluid
276+
277+
A link may have more than one geometric element associated to it. Now
278+
there is a stack of two boxes:
279+
280+
.. code-block:: python
281+
282+
from compas.geometry import Box, Frame
283+
from compas.robots import RobotModel
284+
from compas_rhino.artists import RobotModelArtist
285+
286+
model = RobotModel(name='Boxy')
287+
box_1 = Box(Frame([0, .5, .25], [1, 0, 0], [0, 1, 0]), 1, 2, .5)
288+
box_2 = Box(Frame([0, 0, 4], [1, 0, 0], [0, 1, 0]), .5, 1, 7)
289+
model.add_link(name='box_link', visual_meshes=[box_1, box_2])
290+
291+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
292+
artist.clear_layer()
293+
artist.draw_visual()
294+
295+
.. figure:: files/boxy_3.png
296+
:figclass: figure
297+
:class: figure-img img-fluid
298+
299+
Remember that the frame of the box is the geometric center of the box relative
300+
to the link's origin (which, in this case, happens to be the world XY frame).
301+
So to stack the boxes, the center of ``box_2`` must be placed at a height of
302+
``<height of box_1> + 1/2 * <height of box_2> = .5 + .5 * 7 = 4``.
303+
304+
One link does not an interesting robot make. The following code snippet adds
305+
a cylindrical second link as well as a joint connecting the two.
306+
307+
.. code-block:: python
308+
309+
from compas.geometry import Box, Circle, Cylinder, Frame, Plane, Vector
310+
from compas.robots import Joint, RobotModel
311+
from compas_rhino.artists import RobotModelArtist
312+
313+
model = RobotModel(name='Jointy')
314+
box_1 = Box(Frame([2, .5, .25], [1, 0, 0], [0, 1, 0]), 1, 2, .5)
315+
box_2 = Box(Frame([2, 0, 4], [1, 0, 0], [0, 1, 0]), .5, 1, 7)
316+
box_link = model.add_link(name='box_link', visual_meshes=[box_1, box_2])
317+
318+
cylinder = Cylinder(Circle(Plane([0, 0, 0], [0, 0, 1]), .5), 8)
319+
cylinder_link = model.add_link(name='cylinder_link', visual_meshes=[cylinder])
320+
321+
origin = Frame([0, 0, 7], [1, 0, 0], [0, 1, 0])
322+
axis = Vector(1, 0, 0)
323+
model.add_joint(
324+
name='box_cylinder_joint',
325+
type=Joint.CONTINUOUS,
326+
parent_link=box_link,
327+
child_link=cylinder_link,
328+
origin=origin,
329+
axis=axis,
330+
)
331+
332+
artist = RobotModelArtist(model, layer='COMPAS::Example Robot')
333+
artist.clear_layer()
334+
artist.draw_visual()
335+
336+
337+
.. figure:: files/jointy.png
338+
:figclass: figure
339+
:class: figure-img img-fluid
340+
341+
There's a lot going on in this snippet, so let's break it down. First, the link
342+
containing the stacked boxes is added, but the geometry has been shifted to the
343+
side ``2`` units in the x-direction. Second, a cylindrical link is added.
344+
The cylinder has height ``8`` and radius ``.5`` and is vertically oriented. Finally, a
345+
joint is added. This is the most involved operation so far, so let's explore
346+
this in detail.
347+
348+
Several parameters must be defined for the joint to be sensible.
349+
The required parameters for a minimally defined joint are the ``name``, ``type``,
350+
``parent_link`` and ``child_link``. The location and orientation of the joint is defined
351+
by the ``origin``, which specifies a frame relative to the origin of the ``parent_link``.
352+
In this case, the location of the joint is located ``7`` units above (ie,
353+
in the z-direction) the origin of the ``box_link``, making it ``.5`` units below the top
354+
of the second box. The origin of the joint also specifies the origin of its
355+
``child_link``. Here, this means that the center of the cylinder will coincide with
356+
the location of the joint. Since the geometries of the boxes have been shifted,
357+
the boxes and the cylinder do not overlap. If the joint origin is not specified,
358+
it will default to coincide with the origin of the ``parent_link``. Since a continuous
359+
joint was added, it makes sense that the axis of rotation would also need to be given.
360+
Here, the joint will rotate about the joint origin's x-axis.
361+
362+
Adding a bit more geometry and a few links with fixed joints and we arrive at a rough
363+
model of the classic drinking bird toy (full code
364+
:download:`here <files/drinking_bird.py>`).
365+
366+
.. figure:: files/drinking_bird.png
367+
:figclass: figure
368+
:class: figure-img img-fluid
369+
370+
While it can be useful to programmatically create a robot, more often than not, robot
371+
models are loaded from URDF files. To load a URDF into a robot model, use the
207372
``from_urdf_file`` method::
208373

209374
>>> model = RobotModel.from_urdf_file('ur5.urdf')
@@ -249,33 +414,6 @@ The following snippet shows how to load the robot model currently active in ROS:
249414
... print(robot.model)
250415

251416

252-
Visualizing Robots
253-
==================
254-
255-
Once a robot has been built or loaded, it can be visualized in Blender, Rhino or
256-
Grasshopper using one of COMPAS's artists. The basic procedure is the same in
257-
any of the CAD software (aside from the import statement), so for simplicity we
258-
will demonstrate the use of :class:`compas_rhino.artists.RobotModelArtist` in Rhino. Once
259-
COMPAS has been installed for Rhino,
260-
the following can be run in a Python script editor within Rhino.
261-
262-
.. code-block:: python
263-
264-
import compas
265-
from compas.robots import GithubPackageMeshLoader
266-
from compas.robots import RobotModel
267-
from compas_rhino.artists import RobotModelArtist
268-
269-
compas.PRECISION = '12f'
270-
github = GithubPackageMeshLoader('ros-industrial/abb', 'abb_irb6600_support', 'kinetic-devel')
271-
model = RobotModel.from_urdf_file(github.load_urdf('irb6640.urdf'))
272-
model.load_geometry(github)
273-
274-
artist = RobotModelArtist(model, layer='COMPAS FAB::Example')
275-
artist.clear_layer()
276-
artist.draw_visual()
277-
278-
279417
FK, IK & Path Planning
280418
======================
281419

src/compas/geometry/shapes/capsule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def radius(self, radius):
107107

108108
@property
109109
def length(self):
110-
self.line.length
110+
return self.line.length
111111

112112
@property
113113
def volume(self):

src/compas/robots/model/geometry.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ def from_data(cls, data):
235235
box.data = data
236236
return box
237237

238+
@classmethod
239+
def from_geometry(cls, geometry):
240+
return cls(' '.join([str(geometry.xsize), str(geometry.ysize), str(geometry.zsize)]))
241+
238242

239243
class Cylinder(BaseShape):
240244
"""3D shape primitive representing a cylinder.
@@ -295,6 +299,10 @@ def from_data(cls, data):
295299
cyl.data = data
296300
return cyl
297301

302+
@classmethod
303+
def from_geometry(cls, geometry):
304+
return cls(geometry.radius, geometry.height)
305+
298306

299307
class Sphere(BaseShape):
300308
"""3D shape primitive representing a sphere.
@@ -346,6 +354,10 @@ def from_data(cls, data):
346354
sph.data = data
347355
return sph
348356

357+
@classmethod
358+
def from_geometry(cls, geometry):
359+
return cls(geometry.radius)
360+
349361

350362
class Capsule(BaseShape):
351363
"""3D shape primitive representing a capsule.
@@ -397,6 +409,10 @@ def from_data(cls, data):
397409
cap.data = data
398410
return cap
399411

412+
@classmethod
413+
def from_geometry(cls, geometry):
414+
return cls(geometry.radius, geometry.length)
415+
400416

401417
class MeshDescriptor(BaseShape):
402418
"""Description of a mesh.

0 commit comments

Comments
 (0)