rod 0.2.dev12__tar.gz → 0.2.1.dev33__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rod-0.2.dev12 → rod-0.2.1.dev33}/.github/workflows/ci_cd.yml +7 -2
- {rod-0.2.dev12 → rod-0.2.1.dev33}/PKG-INFO +2 -1
- {rod-0.2.dev12 → rod-0.2.1.dev33}/setup.cfg +1 -0
- rod-0.2.1.dev33/src/rod/__init__.py +108 -0
- rod-0.2.1.dev33/src/rod/builder/primitives.py +110 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/tree_transforms.py +46 -25
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/model.py +2 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/urdf/exporter.py +86 -115
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/frame_convention.py +96 -13
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/gazebo.py +13 -3
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/resolve_frames.py +7 -1
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/PKG-INFO +2 -1
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/SOURCES.txt +2 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/requires.txt +1 -0
- rod-0.2.1.dev33/tests/test_meshbuilder.py +61 -0
- rod-0.2.1.dev33/tests/test_urdf_exporter.py +125 -0
- rod-0.2.dev12/src/rod/__init__.py +0 -51
- rod-0.2.dev12/src/rod/builder/primitives.py +0 -56
- {rod-0.2.dev12 → rod-0.2.1.dev33}/.github/workflows/style.yml +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/.gitignore +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/LICENSE +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/README.md +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/pyproject.toml +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/setup.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/builder/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/builder/primitive_builder.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/kinematic_tree.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/logging.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/pretty_printer.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/collision.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/common.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/element.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/geometry.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/joint.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/link.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/material.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/physics.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/scene.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/sdf.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/visual.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/world.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/directed_tree.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/tree_elements.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/urdf/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/__init__.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/resolve_uris.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/dependency_links.txt +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/not-zip-safe +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/top_level.txt +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/tests/test_urdf_parsing.py +0 -0
- {rod-0.2.dev12 → rod-0.2.1.dev33}/tests/utils_models.py +0 -0
|
@@ -104,7 +104,11 @@ jobs:
|
|
|
104
104
|
if: matrix.type == 'apt'
|
|
105
105
|
run: |
|
|
106
106
|
sudo apt-get update
|
|
107
|
-
sudo apt-get install -
|
|
107
|
+
sudo apt-get install lsb-release wget gnupg
|
|
108
|
+
wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
|
|
109
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
|
|
110
|
+
sudo apt-get update
|
|
111
|
+
sudo apt-get install --no-install-recommends libsdformat13 gz-tools2
|
|
108
112
|
|
|
109
113
|
- name: Install conda dependencies
|
|
110
114
|
if: matrix.type == 'conda'
|
|
@@ -122,7 +126,8 @@ jobs:
|
|
|
122
126
|
pptree \
|
|
123
127
|
idyntree \
|
|
124
128
|
pytest \
|
|
125
|
-
robot_descriptions
|
|
129
|
+
robot_descriptions \
|
|
130
|
+
trimesh
|
|
126
131
|
# pytest-icdiff \ # creates problems on macOS
|
|
127
132
|
mamba install -y gz-sim7 idyntree
|
|
128
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rod
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev33
|
|
4
4
|
Summary: The ultimate Python tool for RObot Descriptions processing.
|
|
5
5
|
Home-page: https://github.com/ami-iit/rod
|
|
6
6
|
Author: Diego Ferigo
|
|
@@ -38,6 +38,7 @@ Requires-Dist: numpy
|
|
|
38
38
|
Requires-Dist: packaging
|
|
39
39
|
Requires-Dist: resolve-robotics-uri-py
|
|
40
40
|
Requires-Dist: scipy
|
|
41
|
+
Requires-Dist: trimesh
|
|
41
42
|
Requires-Dist: xmltodict
|
|
42
43
|
Provides-Extra: style
|
|
43
44
|
Requires-Dist: black; extra == "style"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from . import logging
|
|
2
|
+
from .sdf.collision import Collision
|
|
3
|
+
from .sdf.common import Frame, Pose, Xyz
|
|
4
|
+
from .sdf.geometry import (
|
|
5
|
+
Box,
|
|
6
|
+
Capsule,
|
|
7
|
+
Cylinder,
|
|
8
|
+
Ellipsoid,
|
|
9
|
+
Geometry,
|
|
10
|
+
Heightmap,
|
|
11
|
+
Mesh,
|
|
12
|
+
Plane,
|
|
13
|
+
Sphere,
|
|
14
|
+
)
|
|
15
|
+
from .sdf.joint import Axis, Dynamics, Joint, Limit
|
|
16
|
+
from .sdf.link import Inertia, Inertial, Link
|
|
17
|
+
from .sdf.material import Material
|
|
18
|
+
from .sdf.model import Model
|
|
19
|
+
from .sdf.physics import Physics
|
|
20
|
+
from .sdf.scene import Scene
|
|
21
|
+
from .sdf.sdf import Sdf
|
|
22
|
+
from .sdf.visual import Visual
|
|
23
|
+
from .sdf.world import World
|
|
24
|
+
from .utils.frame_convention import FrameConvention
|
|
25
|
+
|
|
26
|
+
# ===============================
|
|
27
|
+
# Configure the logging verbosity
|
|
28
|
+
# ===============================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_editable():
|
|
32
|
+
"""
|
|
33
|
+
Check if the rod package is installed in editable mode.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import importlib.util
|
|
37
|
+
import pathlib
|
|
38
|
+
import site
|
|
39
|
+
|
|
40
|
+
# Get the ModuleSpec of rod
|
|
41
|
+
rod_spec = importlib.util.find_spec(name="rod")
|
|
42
|
+
|
|
43
|
+
# This can be None. If it's None, assume non-editable installation.
|
|
44
|
+
if rod_spec.origin is None:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Get the folder containing the rod package
|
|
48
|
+
rod_package_dir = str(pathlib.Path(rod_spec.origin).parent.parent)
|
|
49
|
+
|
|
50
|
+
# The installation is editable if the package dir is not in any {site|dist}-packages
|
|
51
|
+
return rod_package_dir not in site.getsitepackages()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Initialize the logging verbosity depending on the installation mode.
|
|
55
|
+
logging.configure(
|
|
56
|
+
level=logging.LoggingLevel.DEBUG if _is_editable() else logging.LoggingLevel.WARNING
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
del _is_editable
|
|
60
|
+
|
|
61
|
+
# =====================================
|
|
62
|
+
# Check for compatible sdformat version
|
|
63
|
+
# =====================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_compatible_sdformat(specification_version: str) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Check if the installed sdformat version produces SDF files compatible with ROD.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
specification_version: The minimum required SDF specification version.
|
|
72
|
+
|
|
73
|
+
Note:
|
|
74
|
+
This check runs only if sdformat is installed in the system.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
import packaging.version
|
|
80
|
+
import xmltodict
|
|
81
|
+
|
|
82
|
+
from rod.utils.gazebo import GazeboHelper
|
|
83
|
+
|
|
84
|
+
if os.environ.get("ROD_SKIP_SDFORMAT_CHECK", "0") == "1":
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if not GazeboHelper.has_gazebo():
|
|
88
|
+
return
|
|
89
|
+
else:
|
|
90
|
+
cmdline = GazeboHelper.get_gazebo_executable()
|
|
91
|
+
logging.info(f"Calling sdformat through '{cmdline} sdf'")
|
|
92
|
+
|
|
93
|
+
output_sdf_version = packaging.version.Version(
|
|
94
|
+
xmltodict.parse(
|
|
95
|
+
xml_input=GazeboHelper.process_model_description_with_sdformat(
|
|
96
|
+
model_description="<sdf version='1.4'/>"
|
|
97
|
+
)
|
|
98
|
+
)["sdf"]["@version"]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if output_sdf_version < packaging.version.Version(specification_version):
|
|
102
|
+
msg = "The found sdformat installation only supports the '{}' specification, "
|
|
103
|
+
msg += "while ROD requires at least the '{}' specification."
|
|
104
|
+
raise RuntimeError(msg.format(output_sdf_version, specification_version))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
check_compatible_sdformat(specification_version="1.10")
|
|
108
|
+
del check_compatible_sdformat
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import trimesh
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
|
|
8
|
+
import rod
|
|
9
|
+
from rod.builder.primitive_builder import PrimitiveBuilder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class SphereBuilder(PrimitiveBuilder):
|
|
14
|
+
radius: float
|
|
15
|
+
|
|
16
|
+
def _inertia(self) -> rod.Inertia:
|
|
17
|
+
return rod.Inertia(
|
|
18
|
+
ixx=2 / 5 * self.mass * (self.radius) ** 2,
|
|
19
|
+
iyy=2 / 5 * self.mass * (self.radius) ** 2,
|
|
20
|
+
izz=2 / 5 * self.mass * (self.radius) ** 2,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _geometry(self) -> rod.Geometry:
|
|
24
|
+
return rod.Geometry(sphere=rod.Sphere(radius=self.radius))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclasses.dataclass
|
|
28
|
+
class BoxBuilder(PrimitiveBuilder):
|
|
29
|
+
x: float
|
|
30
|
+
y: float
|
|
31
|
+
z: float
|
|
32
|
+
|
|
33
|
+
def _inertia(self) -> rod.Inertia:
|
|
34
|
+
return rod.Inertia(
|
|
35
|
+
ixx=self.mass / 12 * (self.y**2 + self.z**2),
|
|
36
|
+
iyy=self.mass / 12 * (self.x**2 + self.z**2),
|
|
37
|
+
izz=self.mass / 12 * (self.x**2 + self.y**2),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _geometry(self) -> rod.Geometry:
|
|
41
|
+
return rod.Geometry(box=rod.Box(size=[self.x, self.y, self.z]))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass
|
|
45
|
+
class CylinderBuilder(PrimitiveBuilder):
|
|
46
|
+
radius: float
|
|
47
|
+
length: float
|
|
48
|
+
|
|
49
|
+
def _inertia(self) -> rod.Inertia:
|
|
50
|
+
ixx_iyy = self.mass * (3 * self.radius**2 + self.length**2) / 12
|
|
51
|
+
|
|
52
|
+
return rod.Inertia(
|
|
53
|
+
ixx=ixx_iyy,
|
|
54
|
+
iyy=ixx_iyy,
|
|
55
|
+
izz=0.5 * self.mass * self.radius**2,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _geometry(self) -> rod.Geometry:
|
|
59
|
+
return rod.Geometry(
|
|
60
|
+
cylinder=rod.Cylinder(radius=self.radius, length=self.length)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass
|
|
65
|
+
class MeshBuilder(PrimitiveBuilder):
|
|
66
|
+
mesh_path: Union[str, pathlib.Path]
|
|
67
|
+
scale: NDArray
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Post-initialization method for the class.
|
|
72
|
+
Loads the mesh from the specified file path and performs necessary checks.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
AssertionError: If the scale is not a 3D vector.
|
|
76
|
+
TypeError: If the mesh_path is not a str or pathlib.Path.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if isinstance(self.mesh_path, str):
|
|
80
|
+
extension = pathlib.Path(self.mesh_path).suffix
|
|
81
|
+
elif isinstance(self.mesh_path, pathlib.Path):
|
|
82
|
+
extension = self.mesh_path.suffix
|
|
83
|
+
else:
|
|
84
|
+
raise TypeError(
|
|
85
|
+
f"Expected str or pathlib.Path for mesh_path, got {type(self.mesh_path)}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.mesh: trimesh.base.Trimesh = trimesh.load(
|
|
89
|
+
str(self.mesh_path),
|
|
90
|
+
force="mesh",
|
|
91
|
+
file_type=extension,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert self.scale.shape == (
|
|
95
|
+
3,
|
|
96
|
+
), f"Scale must be a 3D vector, got {self.scale.shape}"
|
|
97
|
+
|
|
98
|
+
def _inertia(self) -> rod.Inertia:
|
|
99
|
+
inertia = self.mesh.moment_inertia
|
|
100
|
+
return rod.Inertia(
|
|
101
|
+
ixx=inertia[0, 0],
|
|
102
|
+
ixy=inertia[0, 1],
|
|
103
|
+
ixz=inertia[0, 2],
|
|
104
|
+
iyy=inertia[1, 1],
|
|
105
|
+
iyz=inertia[1, 2],
|
|
106
|
+
izz=inertia[2, 2],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _geometry(self) -> rod.Geometry:
|
|
110
|
+
return rod.Geometry(mesh=rod.Mesh(uri=str(self.mesh_path), scale=self.scale))
|
|
@@ -14,18 +14,15 @@ class TreeTransforms:
|
|
|
14
14
|
kinematic_tree: KinematicTree = dataclasses.dataclass(init=False)
|
|
15
15
|
|
|
16
16
|
@staticmethod
|
|
17
|
-
def build(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
prevent_switching_frame_convention: bool = False,
|
|
21
|
-
) -> "TreeTransforms":
|
|
17
|
+
def build(model: "rod.Model", is_top_level: bool = True) -> "TreeTransforms":
|
|
18
|
+
|
|
19
|
+
# Operate on a deep copy of the model to avoid side effects.
|
|
22
20
|
model = copy.deepcopy(model)
|
|
23
21
|
|
|
22
|
+
# Make sure that all elements have a pose attribute with explicit 'relative_to'.
|
|
24
23
|
model.resolve_frames(is_top_level=is_top_level, explicit_frames=True)
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
model.switch_frame_convention(frame_convention=rod.FrameConvention.Urdf)
|
|
28
|
-
|
|
25
|
+
# Build the kinematic tree and return the TreeTransforms object.
|
|
29
26
|
return TreeTransforms(
|
|
30
27
|
kinematic_tree=KinematicTree.build(model=model, is_top_level=is_top_level)
|
|
31
28
|
)
|
|
@@ -54,30 +51,54 @@ class TreeTransforms:
|
|
|
54
51
|
|
|
55
52
|
return W_H_E
|
|
56
53
|
|
|
57
|
-
if (
|
|
58
|
-
name in self.kinematic_tree.link_names()
|
|
59
|
-
or name in self.kinematic_tree.frame_names()
|
|
60
|
-
):
|
|
61
|
-
element = (
|
|
62
|
-
self.kinematic_tree.links_dict[name]
|
|
63
|
-
if name in self.kinematic_tree.link_names()
|
|
64
|
-
else self.kinematic_tree.frames_dict[name]
|
|
65
|
-
)
|
|
66
|
-
assert element.name() == name
|
|
54
|
+
if name in self.kinematic_tree.link_names():
|
|
67
55
|
|
|
68
|
-
|
|
56
|
+
element = self.kinematic_tree.links_dict[name]
|
|
57
|
+
|
|
58
|
+
assert element.name() == name
|
|
69
59
|
assert element._source.pose.relative_to not in {"", None}
|
|
70
|
-
|
|
60
|
+
|
|
61
|
+
# Get the pose of the frame in which the link's pose is expressed.
|
|
62
|
+
x_H_L = element._source.pose.transform()
|
|
71
63
|
W_H_x = self.transform(name=element._source.pose.relative_to)
|
|
72
64
|
|
|
73
|
-
# Compute
|
|
74
|
-
|
|
65
|
+
# Compute the world transform of the link.
|
|
66
|
+
W_H_L = W_H_x @ x_H_L
|
|
67
|
+
return W_H_L
|
|
68
|
+
|
|
69
|
+
if name in self.kinematic_tree.frame_names():
|
|
70
|
+
|
|
71
|
+
element = self.kinematic_tree.frames_dict[name]
|
|
72
|
+
|
|
73
|
+
assert element.name() == name
|
|
74
|
+
assert element._source.pose.relative_to not in {"", None}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
# Get the pose of the frame in which the frame's pose is expressed.
|
|
77
|
+
x_H_F = element._source.pose.transform()
|
|
78
|
+
W_H_x = self.transform(name=element._source.pose.relative_to)
|
|
79
|
+
|
|
80
|
+
# Compute the world transform of the frame.
|
|
81
|
+
W_H_F = W_H_x @ x_H_F
|
|
82
|
+
return W_H_F
|
|
77
83
|
|
|
78
84
|
raise ValueError(name)
|
|
79
85
|
|
|
80
86
|
def relative_transform(self, relative_to: str, name: str) -> npt.NDArray:
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
|
|
88
|
+
world_H_name = self.transform(name=name)
|
|
89
|
+
world_H_relative_to = self.transform(name=relative_to)
|
|
90
|
+
|
|
91
|
+
return TreeTransforms.inverse(world_H_relative_to) @ world_H_name
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def inverse(transform: npt.NDArray) -> npt.NDArray:
|
|
95
|
+
|
|
96
|
+
R = transform[0:3, 0:3]
|
|
97
|
+
p = np.vstack(transform[0:3, 3])
|
|
98
|
+
|
|
99
|
+
return np.block(
|
|
100
|
+
[
|
|
101
|
+
[R.T, -R.T @ p],
|
|
102
|
+
[0, 0, 0, 1],
|
|
103
|
+
]
|
|
83
104
|
)
|
|
@@ -158,6 +158,7 @@ class Model(Element):
|
|
|
158
158
|
frame_convention: "rod.FrameConvention",
|
|
159
159
|
is_top_level: bool = True,
|
|
160
160
|
explicit_frames: bool = True,
|
|
161
|
+
attach_frames_to_links: bool = True,
|
|
161
162
|
) -> None:
|
|
162
163
|
from rod.utils.frame_convention import switch_frame_convention
|
|
163
164
|
|
|
@@ -165,6 +166,7 @@ class Model(Element):
|
|
|
165
166
|
model=self,
|
|
166
167
|
frame_convention=frame_convention,
|
|
167
168
|
is_top_level=is_top_level,
|
|
169
|
+
attach_frames_to_links=attach_frames_to_links,
|
|
168
170
|
)
|
|
169
171
|
|
|
170
172
|
self.resolve_frames(is_top_level=is_top_level, explicit_frames=explicit_frames)
|