linkforge-core 1.4.0__tar.gz → 1.4.2__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.
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/.gitignore +3 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/PKG-INFO +14 -34
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/README.md +13 -33
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/pyproject.toml +1 -1
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/__init__.py +3 -3
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/xml_utils.py +6 -25
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/link_builder.py +1 -1
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/semantic_builder.py +26 -20
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/constants.py +2 -21
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/exceptions.py +0 -6
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/urdf_generator.py +0 -39
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/link.py +16 -15
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/robot.py +6 -4
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/sensor.py +8 -1
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/parsers/urdf_parser.py +10 -10
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/parsers/xacro_parser.py +116 -9
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/parsers/xml_base.py +2 -2
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/validation/security.py +5 -1
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/LICENSE +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/dict_utils.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/math_utils.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/path_utils.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/_utils/string_utils.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/base.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/helpers.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/interfaces.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/robot_builder.py +3 -3
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/srdf_generator.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/xacro_generator.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/xml_base.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/io.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/logging_config.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/gazebo.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/geometry.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/graph.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/joint.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/material.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/ros2_control.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/srdf.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/models/transmission.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/parsers/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/parsers/srdf_parser.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/physics/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/physics/inertia.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/physics/mesh_validation.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/py.typed +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/validation/__init__.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/validation/checks.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/validation/result.py +0 -0
- {linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/validation/validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linkforge-core
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: LinkForge Core: Programmable IR and Physical Validation Engine for Robotics
|
|
5
5
|
Project-URL: Homepage, https://github.com/arounamounchili/linkforge
|
|
6
6
|
Project-URL: Documentation, https://linkforge.readthedocs.io/
|
|
@@ -37,17 +37,13 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
<a href="https://github.com/arounamounchili/linkforge/blob/main/core/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License"></a>
|
|
38
38
|
</p>
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## 🔭 What Is LinkForge Core?
|
|
40
|
+
## What Is LinkForge Core?
|
|
43
41
|
|
|
44
42
|
Writing and maintaining URDF or SRDF by hand is fragile: inertia values are guessed, collision geometries drift, and physics bugs surface only after a simulator crash (or worse, on hardware). LinkForge Core solves this by treating your robot as **source code with physical constraints**, not a static XML document.
|
|
45
43
|
|
|
46
44
|
It provides a mathematically rigorous, zero-dependency Intermediate Representation (IR) engine with hardened physical validation, scientific inertia solvers (Mirtich / Sylvester), and lossless round-trip translation between **URDF**, **XACRO**, and **SRDF**.
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## 📦 Installation
|
|
46
|
+
## Installation
|
|
51
47
|
|
|
52
48
|
```bash
|
|
53
49
|
pip install linkforge-core
|
|
@@ -55,18 +51,14 @@ pip install linkforge-core
|
|
|
55
51
|
|
|
56
52
|
Zero external dependencies. No Blender, no ROS installation, no C++ compilation required.
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
## ⚡ Why LinkForge Core?
|
|
54
|
+
## Why LinkForge Core?
|
|
61
55
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
56
|
+
- **Physically Guaranteed Sim Stability**: Zero-mass links or unphysical inertia tensors cause simulators like Gazebo or Isaac Sim to crash. LinkForge Core uses the **Mirtich algorithm** (Divergence Theorem) to calculate exact inertia properties from geometries, validated against **Sylvester's Criterion** to ensure physical validity.
|
|
57
|
+
- **Standardized & Namespaced Assembly**: Easily compile complex robots, merge multiple sub-assemblies (e.g. attaching a gripper to an arm), and apply joint prefixing and limits programmatically using the fluent **Composer API**.
|
|
58
|
+
- **Hardened Sandboxed Security**: Safely parse untrusted third-party robot descriptions. LinkForge Core blocks path-traversal attacks and restrains file reading to designated package boundaries.
|
|
59
|
+
- **Light & Portable**: Zero external dependencies. No C++ compilation required, making it highly portable across standard Python environments, CI/CD pipelines, and HPC clusters.
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
## 🚀 Quickstart
|
|
61
|
+
## Quickstart
|
|
70
62
|
|
|
71
63
|
LinkForge Core exposes a flat, curated public API. No nested import paths required.
|
|
72
64
|
|
|
@@ -100,9 +92,7 @@ builder.link("upper_arm", parent="base_link") \
|
|
|
100
92
|
urdf_xml = builder.export_urdf()
|
|
101
93
|
```
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
## 💎 Key Capabilities
|
|
95
|
+
## Key Capabilities
|
|
106
96
|
|
|
107
97
|
### Parse, Validate & Compile (Full Round-Trip)
|
|
108
98
|
|
|
@@ -126,8 +116,6 @@ else:
|
|
|
126
116
|
print(f" [{issue.code.name}] {issue.message} on {issue.affected_objects}")
|
|
127
117
|
```
|
|
128
118
|
|
|
129
|
-
---
|
|
130
|
-
|
|
131
119
|
### MoveIt 2 & SRDF Semantic Composition
|
|
132
120
|
|
|
133
121
|
Programmatically compose MoveIt 2 planning groups, end-effectors, and self-collision matrices for SRDF without manually editing XML:
|
|
@@ -162,8 +150,6 @@ semantic = read_srdf("my_robot.srdf")
|
|
|
162
150
|
write_srdf(semantic, "my_robot_updated.srdf")
|
|
163
151
|
```
|
|
164
152
|
|
|
165
|
-
---
|
|
166
|
-
|
|
167
153
|
### Exact Solid-Body Inertia Solver
|
|
168
154
|
|
|
169
155
|
Compute principal moments of inertia and Center of Mass offsets for primitives or complex triangle meshes, hardened with local origin conditioning for floating-point accuracy:
|
|
@@ -176,8 +162,6 @@ geometry = Box(size=Vector3(1.0, 0.5, 0.3))
|
|
|
176
162
|
inertia = calculate_inertia(geometry, mass=10.0)
|
|
177
163
|
```
|
|
178
164
|
|
|
179
|
-
---
|
|
180
|
-
|
|
181
165
|
### Sensor Suite
|
|
182
166
|
|
|
183
167
|
Define cameras, LiDAR, IMU, GPS, and force/torque sensors with configurable noise models directly in the IR:
|
|
@@ -199,8 +183,6 @@ builder.link("camera_link", parent="base_link") \
|
|
|
199
183
|
.commit()
|
|
200
184
|
```
|
|
201
185
|
|
|
202
|
-
---
|
|
203
|
-
|
|
204
186
|
### Headless Use in CI / ML Pipelines
|
|
205
187
|
|
|
206
188
|
`linkforge-core` has zero GUI dependencies, making it ideal for headless environments:
|
|
@@ -227,10 +209,8 @@ pip install linkforge-core
|
|
|
227
209
|
python ci_validate.py
|
|
228
210
|
```
|
|
229
211
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## 📚 Resources & Documentation
|
|
212
|
+
## Resources & Documentation
|
|
233
213
|
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
214
|
+
- **Extensive Documentation**: Read the tutorials and how-to guides at [linkforge.readthedocs.io](https://linkforge.readthedocs.io/).
|
|
215
|
+
- **Open Source Repository**: View source, open issues, and join discussions on [GitHub](https://github.com/arounamounchili/linkforge).
|
|
216
|
+
- **License**: Standard open-source **[Apache-2.0 License](https://github.com/arounamounchili/linkforge/blob/main/core/LICENSE)**.
|
|
@@ -8,17 +8,13 @@
|
|
|
8
8
|
<a href="https://github.com/arounamounchili/linkforge/blob/main/core/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License"></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## 🔭 What Is LinkForge Core?
|
|
11
|
+
## What Is LinkForge Core?
|
|
14
12
|
|
|
15
13
|
Writing and maintaining URDF or SRDF by hand is fragile: inertia values are guessed, collision geometries drift, and physics bugs surface only after a simulator crash (or worse, on hardware). LinkForge Core solves this by treating your robot as **source code with physical constraints**, not a static XML document.
|
|
16
14
|
|
|
17
15
|
It provides a mathematically rigorous, zero-dependency Intermediate Representation (IR) engine with hardened physical validation, scientific inertia solvers (Mirtich / Sylvester), and lossless round-trip translation between **URDF**, **XACRO**, and **SRDF**.
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## 📦 Installation
|
|
17
|
+
## Installation
|
|
22
18
|
|
|
23
19
|
```bash
|
|
24
20
|
pip install linkforge-core
|
|
@@ -26,18 +22,14 @@ pip install linkforge-core
|
|
|
26
22
|
|
|
27
23
|
Zero external dependencies. No Blender, no ROS installation, no C++ compilation required.
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## ⚡ Why LinkForge Core?
|
|
25
|
+
## Why LinkForge Core?
|
|
32
26
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
27
|
+
- **Physically Guaranteed Sim Stability**: Zero-mass links or unphysical inertia tensors cause simulators like Gazebo or Isaac Sim to crash. LinkForge Core uses the **Mirtich algorithm** (Divergence Theorem) to calculate exact inertia properties from geometries, validated against **Sylvester's Criterion** to ensure physical validity.
|
|
28
|
+
- **Standardized & Namespaced Assembly**: Easily compile complex robots, merge multiple sub-assemblies (e.g. attaching a gripper to an arm), and apply joint prefixing and limits programmatically using the fluent **Composer API**.
|
|
29
|
+
- **Hardened Sandboxed Security**: Safely parse untrusted third-party robot descriptions. LinkForge Core blocks path-traversal attacks and restrains file reading to designated package boundaries.
|
|
30
|
+
- **Light & Portable**: Zero external dependencies. No C++ compilation required, making it highly portable across standard Python environments, CI/CD pipelines, and HPC clusters.
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## 🚀 Quickstart
|
|
32
|
+
## Quickstart
|
|
41
33
|
|
|
42
34
|
LinkForge Core exposes a flat, curated public API. No nested import paths required.
|
|
43
35
|
|
|
@@ -71,9 +63,7 @@ builder.link("upper_arm", parent="base_link") \
|
|
|
71
63
|
urdf_xml = builder.export_urdf()
|
|
72
64
|
```
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## 💎 Key Capabilities
|
|
66
|
+
## Key Capabilities
|
|
77
67
|
|
|
78
68
|
### Parse, Validate & Compile (Full Round-Trip)
|
|
79
69
|
|
|
@@ -97,8 +87,6 @@ else:
|
|
|
97
87
|
print(f" [{issue.code.name}] {issue.message} on {issue.affected_objects}")
|
|
98
88
|
```
|
|
99
89
|
|
|
100
|
-
---
|
|
101
|
-
|
|
102
90
|
### MoveIt 2 & SRDF Semantic Composition
|
|
103
91
|
|
|
104
92
|
Programmatically compose MoveIt 2 planning groups, end-effectors, and self-collision matrices for SRDF without manually editing XML:
|
|
@@ -133,8 +121,6 @@ semantic = read_srdf("my_robot.srdf")
|
|
|
133
121
|
write_srdf(semantic, "my_robot_updated.srdf")
|
|
134
122
|
```
|
|
135
123
|
|
|
136
|
-
---
|
|
137
|
-
|
|
138
124
|
### Exact Solid-Body Inertia Solver
|
|
139
125
|
|
|
140
126
|
Compute principal moments of inertia and Center of Mass offsets for primitives or complex triangle meshes, hardened with local origin conditioning for floating-point accuracy:
|
|
@@ -147,8 +133,6 @@ geometry = Box(size=Vector3(1.0, 0.5, 0.3))
|
|
|
147
133
|
inertia = calculate_inertia(geometry, mass=10.0)
|
|
148
134
|
```
|
|
149
135
|
|
|
150
|
-
---
|
|
151
|
-
|
|
152
136
|
### Sensor Suite
|
|
153
137
|
|
|
154
138
|
Define cameras, LiDAR, IMU, GPS, and force/torque sensors with configurable noise models directly in the IR:
|
|
@@ -170,8 +154,6 @@ builder.link("camera_link", parent="base_link") \
|
|
|
170
154
|
.commit()
|
|
171
155
|
```
|
|
172
156
|
|
|
173
|
-
---
|
|
174
|
-
|
|
175
157
|
### Headless Use in CI / ML Pipelines
|
|
176
158
|
|
|
177
159
|
`linkforge-core` has zero GUI dependencies, making it ideal for headless environments:
|
|
@@ -198,10 +180,8 @@ pip install linkforge-core
|
|
|
198
180
|
python ci_validate.py
|
|
199
181
|
```
|
|
200
182
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
## 📚 Resources & Documentation
|
|
183
|
+
## Resources & Documentation
|
|
204
184
|
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
185
|
+
- **Extensive Documentation**: Read the tutorials and how-to guides at [linkforge.readthedocs.io](https://linkforge.readthedocs.io/).
|
|
186
|
+
- **Open Source Repository**: View source, open issues, and join discussions on [GitHub](https://github.com/arounamounchili/linkforge).
|
|
187
|
+
- **License**: Standard open-source **[Apache-2.0 License](https://github.com/arounamounchili/linkforge/blob/main/core/LICENSE)**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "linkforge-core"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.2" # x-release-please-version
|
|
4
4
|
description = "LinkForge Core: Programmable IR and Physical Validation Engine for Robotics"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -11,7 +11,7 @@ for parsing, generating, and validating robot descriptions across formats.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
# Versioning
|
|
14
|
-
__version__ = "1.4.
|
|
14
|
+
__version__ = "1.4.2" # x-release-please-version
|
|
15
15
|
|
|
16
16
|
# Sub-Package Exposure
|
|
17
17
|
# (Ensures lf.models, lf.parsers, etc. are accessible via dot-notation)
|
|
@@ -47,9 +47,9 @@ from .composer import (
|
|
|
47
47
|
|
|
48
48
|
# Core Infrastructure: Constants and Exceptions
|
|
49
49
|
from .constants import (
|
|
50
|
-
DEFAULT_GRAVITY,
|
|
51
50
|
DEFAULT_LINK_MASS,
|
|
52
51
|
EPSILON,
|
|
52
|
+
GRAVITY_ENABLED,
|
|
53
53
|
PI,
|
|
54
54
|
)
|
|
55
55
|
from .exceptions import (
|
|
@@ -293,7 +293,7 @@ __all__ = [
|
|
|
293
293
|
# Constants
|
|
294
294
|
"PI",
|
|
295
295
|
"EPSILON",
|
|
296
|
-
"
|
|
296
|
+
"GRAVITY_ENABLED",
|
|
297
297
|
"DEFAULT_LINK_MASS",
|
|
298
298
|
# Logging
|
|
299
299
|
"get_logger",
|
|
@@ -136,11 +136,14 @@ def serialize_xml(
|
|
|
136
136
|
|
|
137
137
|
# Ensure namespaces are explicitly present on root if ElementTree dropped them
|
|
138
138
|
if namespaces:
|
|
139
|
+
import re
|
|
140
|
+
|
|
139
141
|
for prefix, uri in namespaces.items():
|
|
140
142
|
ns_attr = f'xmlns:{prefix}="{uri}"'
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
if ns_attr not in xml_str:
|
|
144
|
+
xml_str = re.sub(
|
|
145
|
+
rf"<{element.tag}\b", f"<{element.tag} {ns_attr}", xml_str, count=1
|
|
146
|
+
)
|
|
144
147
|
|
|
145
148
|
return get_xml_header(element, version) + xml_str
|
|
146
149
|
|
|
@@ -350,28 +353,6 @@ def xml_add_text(parent: ET.Element, tag: str, value: Any) -> ET.Element:
|
|
|
350
353
|
return elem
|
|
351
354
|
|
|
352
355
|
|
|
353
|
-
def xml_add_vector(
|
|
354
|
-
parent: ET.Element,
|
|
355
|
-
tag: str,
|
|
356
|
-
vector: Vector3,
|
|
357
|
-
formatter: Callable[[float], str],
|
|
358
|
-
) -> ET.Element:
|
|
359
|
-
"""Create a sub-element for a vector relying on a formatter for the values.
|
|
360
|
-
|
|
361
|
-
Args:
|
|
362
|
-
parent: The parent XML element.
|
|
363
|
-
tag: The tag name for the new element.
|
|
364
|
-
vector: The Vector3 object containing the values.
|
|
365
|
-
formatter: A callable that takes a float and returns a string (e.g., format_float).
|
|
366
|
-
|
|
367
|
-
Returns:
|
|
368
|
-
The newly created XML element with the formatted text string.
|
|
369
|
-
"""
|
|
370
|
-
# Create text from formatted components
|
|
371
|
-
text_val = f"{formatter(vector.x)} {formatter(vector.y)} {formatter(vector.z)}"
|
|
372
|
-
return xml_add_text(parent, tag, text_val)
|
|
373
|
-
|
|
374
|
-
|
|
375
356
|
def create_xml_element(
|
|
376
357
|
parent: ET.Element,
|
|
377
358
|
tag: str,
|
|
@@ -983,7 +983,7 @@ class LinkBuilder:
|
|
|
983
983
|
if l_state.inertial_origin is None:
|
|
984
984
|
l_state.inertial_origin = source_origin
|
|
985
985
|
else:
|
|
986
|
-
l_state.inertia = InertiaTensor.
|
|
986
|
+
l_state.inertia = InertiaTensor.stability_floor()
|
|
987
987
|
|
|
988
988
|
return Inertial(
|
|
989
989
|
mass=l_state.mass,
|
{linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/composer/semantic_builder.py
RENAMED
|
@@ -36,6 +36,10 @@ class SemanticBuilder:
|
|
|
36
36
|
"""Initialize semantic builder."""
|
|
37
37
|
self._builder = builder
|
|
38
38
|
|
|
39
|
+
def done(self) -> IComposer:
|
|
40
|
+
"""Return to the parent RobotBuilder."""
|
|
41
|
+
return self._builder
|
|
42
|
+
|
|
39
43
|
def group(
|
|
40
44
|
self,
|
|
41
45
|
name: str,
|
|
@@ -45,7 +49,7 @@ class SemanticBuilder:
|
|
|
45
49
|
subgroups: list[str] | None = None,
|
|
46
50
|
base_link: str | None = None,
|
|
47
51
|
tip_link: str | None = None,
|
|
48
|
-
) ->
|
|
52
|
+
) -> SemanticBuilder:
|
|
49
53
|
"""Define a planning group for MoveIt.
|
|
50
54
|
|
|
51
55
|
Args:
|
|
@@ -75,11 +79,11 @@ class SemanticBuilder:
|
|
|
75
79
|
|
|
76
80
|
semantic = self._builder.robot.semantic
|
|
77
81
|
self._builder.robot.semantic = replace(semantic, groups=tuple(semantic.groups) + (group,))
|
|
78
|
-
return self
|
|
82
|
+
return self
|
|
79
83
|
|
|
80
84
|
def group_state(
|
|
81
85
|
self, name: str, group: str, values: dict[str, float | tuple[float, ...]]
|
|
82
|
-
) ->
|
|
86
|
+
) -> SemanticBuilder:
|
|
83
87
|
"""Define a named state (e.g. 'home') for a planning group.
|
|
84
88
|
|
|
85
89
|
Args:
|
|
@@ -98,11 +102,11 @@ class SemanticBuilder:
|
|
|
98
102
|
self._builder.robot.semantic = replace(
|
|
99
103
|
semantic, group_states=tuple(semantic.group_states) + (state,)
|
|
100
104
|
)
|
|
101
|
-
return self
|
|
105
|
+
return self
|
|
102
106
|
|
|
103
107
|
def end_effector(
|
|
104
108
|
self, name: str, group: str, parent_link: str, parent_group: str | None = None
|
|
105
|
-
) ->
|
|
109
|
+
) -> SemanticBuilder:
|
|
106
110
|
"""Define an end effector for MoveIt.
|
|
107
111
|
|
|
108
112
|
Args:
|
|
@@ -119,9 +123,9 @@ class SemanticBuilder:
|
|
|
119
123
|
self._builder.robot.semantic = replace(
|
|
120
124
|
semantic, end_effectors=tuple(semantic.end_effectors) + (ee,)
|
|
121
125
|
)
|
|
122
|
-
return self
|
|
126
|
+
return self
|
|
123
127
|
|
|
124
|
-
def passive_joint(self, name: str) ->
|
|
128
|
+
def passive_joint(self, name: str) -> SemanticBuilder:
|
|
125
129
|
"""Mark a joint as passive (not actuated) for MoveIt.
|
|
126
130
|
|
|
127
131
|
Args:
|
|
@@ -135,7 +139,7 @@ class SemanticBuilder:
|
|
|
135
139
|
self._builder.robot.semantic = replace(
|
|
136
140
|
semantic, passive_joints=tuple(semantic.passive_joints) + (pj,)
|
|
137
141
|
)
|
|
138
|
-
return self
|
|
142
|
+
return self
|
|
139
143
|
|
|
140
144
|
def virtual_joint(
|
|
141
145
|
self,
|
|
@@ -143,7 +147,7 @@ class SemanticBuilder:
|
|
|
143
147
|
child_link: str,
|
|
144
148
|
parent_frame: str = "world",
|
|
145
149
|
joint_type: str = SRDF_VJOIN_FIXED,
|
|
146
|
-
) ->
|
|
150
|
+
) -> SemanticBuilder:
|
|
147
151
|
"""Define a virtual joint connecting the robot to the world frame.
|
|
148
152
|
|
|
149
153
|
Args:
|
|
@@ -162,11 +166,11 @@ class SemanticBuilder:
|
|
|
162
166
|
self._builder.robot.semantic = replace(
|
|
163
167
|
semantic, virtual_joints=tuple(semantic.virtual_joints) + (vj,)
|
|
164
168
|
)
|
|
165
|
-
return self
|
|
169
|
+
return self
|
|
166
170
|
|
|
167
171
|
def disable_collisions(
|
|
168
172
|
self, link1: str, link2: str, reason: str = SRDF_REASON_ADJACENT
|
|
169
|
-
) ->
|
|
173
|
+
) -> SemanticBuilder:
|
|
170
174
|
"""Instruct MoveIt to ignore collisions between two specific links.
|
|
171
175
|
|
|
172
176
|
Args:
|
|
@@ -181,9 +185,11 @@ class SemanticBuilder:
|
|
|
181
185
|
self._builder.robot.semantic = replace(
|
|
182
186
|
semantic, disabled_collisions=tuple(semantic.disabled_collisions) + (dc,)
|
|
183
187
|
)
|
|
184
|
-
return self
|
|
188
|
+
return self
|
|
185
189
|
|
|
186
|
-
def enable_collisions(
|
|
190
|
+
def enable_collisions(
|
|
191
|
+
self, link1: str, link2: str, reason: str | None = None
|
|
192
|
+
) -> SemanticBuilder:
|
|
187
193
|
"""Explicitly re-enable collision checking between two specific links.
|
|
188
194
|
|
|
189
195
|
Args:
|
|
@@ -198,9 +204,9 @@ class SemanticBuilder:
|
|
|
198
204
|
self._builder.robot.semantic = replace(
|
|
199
205
|
semantic, enabled_collisions=tuple(semantic.enabled_collisions) + (ec,)
|
|
200
206
|
)
|
|
201
|
-
return self
|
|
207
|
+
return self
|
|
202
208
|
|
|
203
|
-
def disable_default_collisions(self, link: str) ->
|
|
209
|
+
def disable_default_collisions(self, link: str) -> SemanticBuilder:
|
|
204
210
|
"""Disable all default collisions for a specific link.
|
|
205
211
|
|
|
206
212
|
Args:
|
|
@@ -214,9 +220,9 @@ class SemanticBuilder:
|
|
|
214
220
|
semantic,
|
|
215
221
|
no_default_collision_links=tuple(semantic.no_default_collision_links) + (link,),
|
|
216
222
|
)
|
|
217
|
-
return self
|
|
223
|
+
return self
|
|
218
224
|
|
|
219
|
-
def joint_property(self, joint_name: str, property_name: str, value: str) ->
|
|
225
|
+
def joint_property(self, joint_name: str, property_name: str, value: str) -> SemanticBuilder:
|
|
220
226
|
"""Add a custom property/metadata to a joint.
|
|
221
227
|
|
|
222
228
|
Args:
|
|
@@ -232,9 +238,9 @@ class SemanticBuilder:
|
|
|
232
238
|
self._builder.robot.semantic = replace(
|
|
233
239
|
semantic, joint_properties=tuple(semantic.joint_properties) + (jp,)
|
|
234
240
|
)
|
|
235
|
-
return self
|
|
241
|
+
return self
|
|
236
242
|
|
|
237
|
-
def approximate_link_collision(self, link: str, spheres: list[SrdfSphere]) ->
|
|
243
|
+
def approximate_link_collision(self, link: str, spheres: list[SrdfSphere]) -> SemanticBuilder:
|
|
238
244
|
"""Add sphere-based collision approximation for a link.
|
|
239
245
|
|
|
240
246
|
Args:
|
|
@@ -250,4 +256,4 @@ class SemanticBuilder:
|
|
|
250
256
|
semantic,
|
|
251
257
|
link_sphere_approximations=tuple(semantic.link_sphere_approximations) + (lsa,),
|
|
252
258
|
)
|
|
253
|
-
return self
|
|
259
|
+
return self
|
|
@@ -75,10 +75,6 @@ MAX_XML_DEPTH: Final[int] = 2000
|
|
|
75
75
|
|
|
76
76
|
# Geometric and Mesh thresholds
|
|
77
77
|
DEGENERATE_VOL_THRESHOLD: Final[float] = 1e-12 # m³
|
|
78
|
-
NEGATIVE_INERTIA_THRESHOLD: Final[float] = -1e-06
|
|
79
|
-
MESH_PROXIMITY_THRESHOLD: Final[int] = 6
|
|
80
|
-
MESH_SLIVER_THRESHOLD: Final[float] = 1000.0
|
|
81
|
-
MIN_MESH_AREA: Final[float] = 1e-15 # m²
|
|
82
78
|
|
|
83
79
|
|
|
84
80
|
# 4. Global Physics Defaults
|
|
@@ -94,7 +90,7 @@ DEFAULT_CONTACT_KP: Final[float] = 1e12 # N/m
|
|
|
94
90
|
DEFAULT_CONTACT_KD: Final[float] = 1.0 # N·s/m
|
|
95
91
|
|
|
96
92
|
# Simulation toggles
|
|
97
|
-
|
|
93
|
+
GRAVITY_ENABLED: Final[bool] = True
|
|
98
94
|
DEFAULT_SELF_COLLIDE: Final[bool] = False
|
|
99
95
|
|
|
100
96
|
|
|
@@ -105,13 +101,11 @@ DEFAULT_SELF_COLLIDE: Final[bool] = False
|
|
|
105
101
|
DEFAULT_LINK_MASS: Final[float] = 1.0 # kg
|
|
106
102
|
DEFAULT_MATERIAL_RGBA: Final[tuple[float, float, float, float]] = (0.7, 0.7, 0.7, 1.0)
|
|
107
103
|
DEFAULT_MATERIAL_RGBA_STR: Final[str] = "0.7 0.7 0.7 1.0"
|
|
108
|
-
DEFAULT_MESH_SCALE_STR: Final[str] = "1 1 1"
|
|
109
104
|
DEFAULT_GEOMETRY_RADIUS: Final[float] = 0.1 # m
|
|
110
105
|
DEFAULT_GEOMETRY_LENGTH: Final[float] = 0.5 # m
|
|
111
106
|
|
|
112
107
|
# Joint Defaults
|
|
113
108
|
DEFAULT_AXIS_XYZ: Final[tuple[float, float, float]] = (0.0, 0.0, 1.0)
|
|
114
|
-
DEFAULT_AXIS_XYZ_STR: Final[str] = "0 0 1"
|
|
115
109
|
DEFAULT_URDF_AXIS_XYZ: Final[tuple[float, float, float]] = (1.0, 0.0, 0.0) # URDF spec default
|
|
116
110
|
DEFAULT_URDF_AXIS_XYZ_STR: Final[str] = "1 0 0"
|
|
117
111
|
|
|
@@ -146,7 +140,6 @@ CAM_FORMAT_BAYER_BGGR8: Final[str] = "BAYER_BGGR8"
|
|
|
146
140
|
|
|
147
141
|
# LIDAR Horizontal Parameters
|
|
148
142
|
DEFAULT_LIDAR_SAMPLES: Final[int] = 640
|
|
149
|
-
DEFAULT_LIDAR_HORIZONTAL_RESOLUTION: Final[float] = 1.0
|
|
150
143
|
DEFAULT_LIDAR_RANGE_MIN: Final[float] = 0.1 # m
|
|
151
144
|
DEFAULT_LIDAR_RANGE_MAX: Final[float] = 10.0 # m
|
|
152
145
|
DEFAULT_LIDAR_RANGE_RESOLUTION: Final[float] = 0.01 # m
|
|
@@ -185,6 +178,7 @@ DEFAULT_JOINT_TYPE: Final[str] = JOINT_REVOLUTE
|
|
|
185
178
|
SENSOR_CAMERA: Final[str] = "camera"
|
|
186
179
|
SENSOR_DEPTH_CAMERA: Final[str] = "depth_camera"
|
|
187
180
|
SENSOR_LIDAR: Final[str] = "lidar"
|
|
181
|
+
SENSOR_RAY: Final[str] = "ray"
|
|
188
182
|
SENSOR_GPU_LIDAR: Final[str] = "gpu_lidar"
|
|
189
183
|
SENSOR_IMU: Final[str] = "imu"
|
|
190
184
|
SENSOR_GPS: Final[str] = "gps"
|
|
@@ -213,20 +207,14 @@ SRDF_VJOIN_PLANAR: Final[str] = "planar"
|
|
|
213
207
|
SRDF_VJOIN_FLOATING: Final[str] = "floating"
|
|
214
208
|
|
|
215
209
|
SRDF_REASON_ADJACENT: Final[str] = "Adjacent"
|
|
216
|
-
SRDF_REASON_NEVER: Final[str] = "Never"
|
|
217
|
-
SRDF_REASON_USER: Final[str] = "User"
|
|
218
|
-
SRDF_REASON_DEFAULT: Final[str] = "Default"
|
|
219
210
|
|
|
220
211
|
# Standard fallback names
|
|
221
212
|
UNNAMED_LINK: Final[str] = "unnamed_link"
|
|
222
213
|
UNNAMED_JOINT: Final[str] = "unnamed_joint"
|
|
223
|
-
UNNAMED_SENSOR: Final[str] = "unnamed_sensor"
|
|
224
214
|
|
|
225
215
|
# Sensor Update Rates (Industry standard defaults)
|
|
226
216
|
DEFAULT_UPDATE_RATE_IMU: Final[float] = 100.0 # Hz
|
|
227
217
|
DEFAULT_UPDATE_RATE_GPS: Final[float] = 5.0 # Hz
|
|
228
|
-
DEFAULT_UPDATE_RATE_LIDAR: Final[float] = 30.0 # Hz
|
|
229
|
-
DEFAULT_UPDATE_RATE_CAMERA: Final[float] = 30.0 # Hz
|
|
230
218
|
DEFAULT_UPDATE_RATE_CONTACT: Final[float] = 50.0 # Hz
|
|
231
219
|
DEFAULT_UPDATE_RATE_FORCE_TORQUE: Final[float] = 100.0 # Hz
|
|
232
220
|
|
|
@@ -261,13 +249,6 @@ GZ_SENSOR_IMU: Final[str] = "imu"
|
|
|
261
249
|
GZ_SENSOR_CONTACT: Final[str] = "contact"
|
|
262
250
|
GZ_SENSOR_FORCE_TORQUE: Final[str] = "force_torque"
|
|
263
251
|
|
|
264
|
-
# Gazebo / GZ XML Elements and Attributes
|
|
265
|
-
GZ_ELEM_GAZEBO: Final[str] = "gazebo"
|
|
266
|
-
GZ_ELEM_SENSOR: Final[str] = "sensor"
|
|
267
|
-
GZ_ELEM_PLUGIN: Final[str] = "plugin"
|
|
268
|
-
GZ_ATTR_REFERENCE: Final[str] = "reference"
|
|
269
|
-
COLLISION_ADJACENT: Final[str] = "Adjacent"
|
|
270
|
-
|
|
271
252
|
# XACRO Parameters and Attributes
|
|
272
253
|
XACRO_PARAM_NAME: Final[str] = "name"
|
|
273
254
|
XACRO_PARAM_PARENT: Final[str] = "parent"
|
|
@@ -29,14 +29,12 @@ class ValidationErrorCode(StrEnum):
|
|
|
29
29
|
HAS_CYCLE = "has_cycle"
|
|
30
30
|
NO_ROOT = "no_root"
|
|
31
31
|
MULTIPLE_ROOTS = "multiple_roots"
|
|
32
|
-
DISCONNECTED = "disconnected"
|
|
33
32
|
|
|
34
33
|
# Physics and Values
|
|
35
34
|
OUT_OF_RANGE = "out_of_range"
|
|
36
35
|
VALUE_EMPTY = "value_empty"
|
|
37
36
|
INVALID_VALUE = "invalid_value"
|
|
38
37
|
PHYSICS_VIOLATION = "physics_violation"
|
|
39
|
-
MATH_ERROR = "math_error"
|
|
40
38
|
INERTIA_TRIANGLE_INEQUALITY = "inertia_triangle_inequality"
|
|
41
39
|
|
|
42
40
|
# Mesh Topology
|
|
@@ -49,7 +47,6 @@ class ValidationErrorCode(StrEnum):
|
|
|
49
47
|
MESH_SLIVER = "mesh_sliver"
|
|
50
48
|
|
|
51
49
|
# Configuration and Misc
|
|
52
|
-
MISMATCH = "mismatch"
|
|
53
50
|
GENERIC_FAILURE = "generic_failure"
|
|
54
51
|
|
|
55
52
|
|
|
@@ -114,7 +111,6 @@ class RobotPhysicsError(RobotModelError):
|
|
|
114
111
|
self.code = code
|
|
115
112
|
self.target = target
|
|
116
113
|
self.value = value
|
|
117
|
-
self.raw_message = message
|
|
118
114
|
|
|
119
115
|
full_msg = f"[PHYSICS_{code.name}] {message}"
|
|
120
116
|
if target:
|
|
@@ -141,7 +137,6 @@ class RobotValidationError(RobotModelError):
|
|
|
141
137
|
self.code = code
|
|
142
138
|
self.target = target
|
|
143
139
|
self.value = value
|
|
144
|
-
self.raw_message = message
|
|
145
140
|
|
|
146
141
|
full_msg = f"[{code.name}] {message}"
|
|
147
142
|
if target:
|
|
@@ -172,7 +167,6 @@ class RobotMathError(RobotModelError):
|
|
|
172
167
|
self.code = code
|
|
173
168
|
self.target = target
|
|
174
169
|
self.value = value
|
|
175
|
-
self.raw_message = message
|
|
176
170
|
|
|
177
171
|
full_msg = f"[MATH_{code.name}] {message}"
|
|
178
172
|
if target:
|
{linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/urdf_generator.py
RENAMED
|
@@ -782,45 +782,6 @@ class URDFGenerator(RobotXMLGenerator):
|
|
|
782
782
|
bias_stddev_elem = ET.SubElement(noise_elem, "bias_stddev")
|
|
783
783
|
bias_stddev_elem.text = format_float(noise.bias_stddev)
|
|
784
784
|
|
|
785
|
-
def _add_gazebo_element(self, parent: ET.Element, gazebo_elem: GazeboElement) -> None:
|
|
786
|
-
"""Add Gazebo extension element to parent.
|
|
787
|
-
|
|
788
|
-
Args:
|
|
789
|
-
parent: Parent XML element
|
|
790
|
-
gazebo_elem: GazeboElement model
|
|
791
|
-
|
|
792
|
-
"""
|
|
793
|
-
# Create gazebo element with optional reference attribute
|
|
794
|
-
attrib: dict[str, str] = {}
|
|
795
|
-
if gazebo_elem.reference is not None:
|
|
796
|
-
attrib["reference"] = gazebo_elem.reference
|
|
797
|
-
|
|
798
|
-
gz_elem = create_xml_element(parent, "gazebo", formatter=self._format_value, **attrib)
|
|
799
|
-
|
|
800
|
-
# Add material if specified
|
|
801
|
-
if gazebo_elem.material is not None:
|
|
802
|
-
xml_add_text(gz_elem, "material", gazebo_elem.material)
|
|
803
|
-
|
|
804
|
-
# Add boolean properties
|
|
805
|
-
self._add_optional_bool_element(gz_elem, "static", gazebo_elem.static)
|
|
806
|
-
self._add_optional_bool_element(gz_elem, "provideFeedback", gazebo_elem.provide_feedback)
|
|
807
|
-
self._add_optional_bool_element(
|
|
808
|
-
gz_elem, "implicitSpringDamper", gazebo_elem.implicit_spring_damper
|
|
809
|
-
)
|
|
810
|
-
|
|
811
|
-
# Add numeric properties
|
|
812
|
-
self._add_optional_numeric_element(gz_elem, "stopCfm", gazebo_elem.stop_cfm)
|
|
813
|
-
self._add_optional_numeric_element(gz_elem, "stopErp", gazebo_elem.stop_erp)
|
|
814
|
-
|
|
815
|
-
# Add custom properties (sort by key for deterministic output)
|
|
816
|
-
for key in sorted(gazebo_elem.properties.keys()):
|
|
817
|
-
prop_elem = ET.SubElement(gz_elem, key)
|
|
818
|
-
prop_elem.text = gazebo_elem.properties[key]
|
|
819
|
-
|
|
820
|
-
# Add plugins (sort by name for deterministic output)
|
|
821
|
-
for plugin in sorted(gazebo_elem.plugins, key=lambda p: p.name):
|
|
822
|
-
self._add_gazebo_plugin_element(gz_elem, plugin)
|
|
823
|
-
|
|
824
785
|
def _add_gazebo_plugin_element(self, parent: ET.Element, plugin: GazeboPlugin) -> None:
|
|
825
786
|
"""Add Gazebo plugin element to parent.
|
|
826
787
|
|
|
@@ -21,9 +21,9 @@ from ..constants import (
|
|
|
21
21
|
DEFAULT_CONTACT_KP,
|
|
22
22
|
DEFAULT_FRICTION_MU,
|
|
23
23
|
DEFAULT_FRICTION_MU2,
|
|
24
|
-
DEFAULT_GRAVITY,
|
|
25
24
|
DEFAULT_SELF_COLLIDE,
|
|
26
25
|
EPSILON,
|
|
26
|
+
GRAVITY_ENABLED,
|
|
27
27
|
MIN_REASONABLE_INERTIA,
|
|
28
28
|
)
|
|
29
29
|
from ..exceptions import RobotPhysicsError, RobotValidationError, ValidationErrorCode
|
|
@@ -46,11 +46,11 @@ class InertiaTensor:
|
|
|
46
46
|
"""
|
|
47
47
|
|
|
48
48
|
ixx: float
|
|
49
|
-
ixy: float
|
|
50
|
-
ixz: float
|
|
51
49
|
iyy: float
|
|
52
|
-
iyz: float
|
|
53
50
|
izz: float
|
|
51
|
+
ixy: float = 0.0
|
|
52
|
+
ixz: float = 0.0
|
|
53
|
+
iyz: float = 0.0
|
|
54
54
|
|
|
55
55
|
def __post_init__(self) -> None:
|
|
56
56
|
"""Validate inertia tensor values."""
|
|
@@ -78,15 +78,16 @@ class InertiaTensor:
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
@classmethod
|
|
81
|
-
def
|
|
82
|
-
"""Create a minimal valid inertia tensor
|
|
81
|
+
def stability_floor(cls) -> InertiaTensor:
|
|
82
|
+
"""Create a minimal valid inertia tensor for stability (e.g. massless links).
|
|
83
|
+
|
|
84
|
+
Returns a tensor with MIN_REASONABLE_INERTIA on the diagonals to prevent
|
|
85
|
+
physics solver instability.
|
|
86
|
+
"""
|
|
83
87
|
return cls(
|
|
84
|
-
MIN_REASONABLE_INERTIA,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
MIN_REASONABLE_INERTIA,
|
|
88
|
-
0.0,
|
|
89
|
-
MIN_REASONABLE_INERTIA,
|
|
88
|
+
ixx=MIN_REASONABLE_INERTIA,
|
|
89
|
+
iyy=MIN_REASONABLE_INERTIA,
|
|
90
|
+
izz=MIN_REASONABLE_INERTIA,
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
|
|
@@ -96,7 +97,7 @@ class Inertial:
|
|
|
96
97
|
|
|
97
98
|
mass: float
|
|
98
99
|
origin: Transform = Transform.identity()
|
|
99
|
-
inertia: InertiaTensor = field(default_factory=InertiaTensor.
|
|
100
|
+
inertia: InertiaTensor = field(default_factory=InertiaTensor.stability_floor)
|
|
100
101
|
|
|
101
102
|
def __post_init__(self) -> None:
|
|
102
103
|
"""Validate inertial properties.
|
|
@@ -122,7 +123,7 @@ class LinkPhysics:
|
|
|
122
123
|
"""
|
|
123
124
|
|
|
124
125
|
self_collide: bool = DEFAULT_SELF_COLLIDE
|
|
125
|
-
gravity: bool =
|
|
126
|
+
gravity: bool = GRAVITY_ENABLED
|
|
126
127
|
mu: float = DEFAULT_FRICTION_MU
|
|
127
128
|
mu2: float = DEFAULT_FRICTION_MU2
|
|
128
129
|
kp: float = DEFAULT_CONTACT_KP
|
|
@@ -211,7 +212,7 @@ class Link:
|
|
|
211
212
|
@property
|
|
212
213
|
def inertia(self) -> InertiaTensor:
|
|
213
214
|
"""Get link inertia tensor (zero tensor if not defined)."""
|
|
214
|
-
return self.inertial.inertia if self.inertial else InertiaTensor.
|
|
215
|
+
return self.inertial.inertia if self.inertial else InertiaTensor.stability_floor()
|
|
215
216
|
|
|
216
217
|
@property
|
|
217
218
|
def inertial_origin(self) -> Transform:
|
|
@@ -24,8 +24,8 @@ from typing import Any
|
|
|
24
24
|
from .._utils.string_utils import is_valid_name
|
|
25
25
|
from ..base import FileSystemResolver, IResourceResolver
|
|
26
26
|
from ..constants import (
|
|
27
|
-
COLLISION_ADJACENT,
|
|
28
27
|
IR_VERSION,
|
|
28
|
+
SRDF_REASON_ADJACENT,
|
|
29
29
|
)
|
|
30
30
|
from ..exceptions import RobotValidationError, ValidationErrorCode
|
|
31
31
|
from .gazebo import GazeboElement
|
|
@@ -856,13 +856,15 @@ class Robot:
|
|
|
856
856
|
self.semantic = replace(self.semantic, groups=tuple(self.semantic.groups) + (group,))
|
|
857
857
|
return self
|
|
858
858
|
|
|
859
|
-
def disable_collisions(
|
|
859
|
+
def disable_collisions(
|
|
860
|
+
self, link1: str, link2: str, reason: str = SRDF_REASON_ADJACENT
|
|
861
|
+
) -> Robot:
|
|
860
862
|
"""Disable collision checking between two links.
|
|
861
863
|
|
|
862
864
|
Args:
|
|
863
865
|
link1: First link name.
|
|
864
866
|
link2: Second link name.
|
|
865
|
-
reason: Reason for disabling (default:
|
|
867
|
+
reason: Reason for disabling (default: SRDF_REASON_ADJACENT).
|
|
866
868
|
|
|
867
869
|
Returns:
|
|
868
870
|
The robot instance for chaining.
|
|
@@ -881,7 +883,7 @@ class Robot:
|
|
|
881
883
|
)
|
|
882
884
|
return self
|
|
883
885
|
|
|
884
|
-
def disable_all_collisions(self, links: list[str], reason: str =
|
|
886
|
+
def disable_all_collisions(self, links: list[str], reason: str = SRDF_REASON_ADJACENT) -> Robot:
|
|
885
887
|
"""Disable collision checking between all pairs in the provided list.
|
|
886
888
|
|
|
887
889
|
Args:
|
|
@@ -50,6 +50,7 @@ from ..constants import (
|
|
|
50
50
|
SENSOR_GPU_LIDAR,
|
|
51
51
|
SENSOR_IMU,
|
|
52
52
|
SENSOR_LIDAR,
|
|
53
|
+
SENSOR_RAY,
|
|
53
54
|
)
|
|
54
55
|
from ..exceptions import RobotValidationError, ValidationErrorCode
|
|
55
56
|
from .gazebo import GazeboPlugin
|
|
@@ -57,11 +58,17 @@ from .geometry import Transform
|
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
class SensorType(StrEnum):
|
|
60
|
-
"""Enumeration of supported sensor types in the LinkForge IR.
|
|
61
|
+
"""Enumeration of supported sensor types in the LinkForge IR.
|
|
62
|
+
|
|
63
|
+
Note on LIDAR vs RAY:
|
|
64
|
+
- LIDAR is the modern Gazebo Harmonic/GZ standard ("lidar")
|
|
65
|
+
- RAY is provided for Gazebo Classic / ROS1 compatibility ("ray")
|
|
66
|
+
"""
|
|
61
67
|
|
|
62
68
|
CAMERA = SENSOR_CAMERA
|
|
63
69
|
DEPTH_CAMERA = SENSOR_DEPTH_CAMERA
|
|
64
70
|
LIDAR = SENSOR_LIDAR
|
|
71
|
+
RAY = SENSOR_RAY
|
|
65
72
|
GPU_LIDAR = SENSOR_GPU_LIDAR
|
|
66
73
|
IMU = SENSOR_IMU
|
|
67
74
|
GPS = SENSOR_GPS
|
|
@@ -1031,13 +1031,13 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1031
1031
|
try:
|
|
1032
1032
|
link = self._parse_link(elem, materials, source_directory)
|
|
1033
1033
|
robot.add_link(link)
|
|
1034
|
-
except (RobotModelError, ValueError
|
|
1034
|
+
except (RobotModelError, ValueError) as e:
|
|
1035
1035
|
logger.warning(f"Skipping invalid link '{elem.get('name')}': {e}")
|
|
1036
1036
|
|
|
1037
1037
|
elif tag == "joint":
|
|
1038
1038
|
try:
|
|
1039
1039
|
delayed_joints.append(self._parse_joint(elem))
|
|
1040
|
-
except (RobotModelError, ValueError
|
|
1040
|
+
except (RobotModelError, ValueError) as e:
|
|
1041
1041
|
logger.warning(f"Skipping invalid joint '{elem.get('name')}': {e}")
|
|
1042
1042
|
|
|
1043
1043
|
elif tag == "transmission":
|
|
@@ -1045,7 +1045,7 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1045
1045
|
trans = self._parse_transmission(elem)
|
|
1046
1046
|
if trans:
|
|
1047
1047
|
delayed_transmissions.append(trans)
|
|
1048
|
-
except (RobotModelError, ValueError
|
|
1048
|
+
except (RobotModelError, ValueError) as e:
|
|
1049
1049
|
logger.warning(
|
|
1050
1050
|
f"Skipping invalid transmission '{elem.get('name')}': {e}"
|
|
1051
1051
|
)
|
|
@@ -1055,7 +1055,7 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1055
1055
|
ros2_ctrl = self._parse_ros2_control(elem)
|
|
1056
1056
|
if ros2_ctrl:
|
|
1057
1057
|
delayed_ros2_controls.append(ros2_ctrl)
|
|
1058
|
-
except (RobotModelError, ValueError
|
|
1058
|
+
except (RobotModelError, ValueError) as e:
|
|
1059
1059
|
logger.warning(
|
|
1060
1060
|
f"Skipping invalid ros2_control '{elem.get('name')}': {e}"
|
|
1061
1061
|
)
|
|
@@ -1082,7 +1082,7 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1082
1082
|
# 3. Extract other Gazebo metadata (plugins, material, properties)
|
|
1083
1083
|
gazebo_elem = self._parse_gazebo_element(elem)
|
|
1084
1084
|
delayed_gazebo_elements.append((gazebo_elem, physics_data))
|
|
1085
|
-
except (RobotModelError, ValueError
|
|
1085
|
+
except (RobotModelError, ValueError) as e:
|
|
1086
1086
|
logger.warning(
|
|
1087
1087
|
f"Skipping invalid gazebo element '{elem.get('name') or elem.get('reference')}': {e}"
|
|
1088
1088
|
)
|
|
@@ -1095,25 +1095,25 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1095
1095
|
for joint in delayed_joints:
|
|
1096
1096
|
try:
|
|
1097
1097
|
robot.add_joint(joint)
|
|
1098
|
-
except
|
|
1098
|
+
except (RobotModelError, ValueError) as e:
|
|
1099
1099
|
logger.warning(f"Skipping invalid joint '{joint.name}': {e}")
|
|
1100
1100
|
|
|
1101
1101
|
for trans in delayed_transmissions:
|
|
1102
1102
|
try:
|
|
1103
1103
|
robot.add_transmission(trans)
|
|
1104
|
-
except
|
|
1104
|
+
except (RobotModelError, ValueError) as e:
|
|
1105
1105
|
logger.warning(f"Skipping invalid transmission '{trans.name}': {e}")
|
|
1106
1106
|
|
|
1107
1107
|
for ros2_ctrl in delayed_ros2_controls:
|
|
1108
1108
|
try:
|
|
1109
1109
|
robot.add_ros2_control(ros2_ctrl)
|
|
1110
|
-
except
|
|
1110
|
+
except (RobotModelError, ValueError) as e:
|
|
1111
1111
|
logger.warning(f"Skipping invalid ros2_control '{ros2_ctrl.name}': {e}")
|
|
1112
1112
|
|
|
1113
1113
|
for sensor in delayed_sensors:
|
|
1114
1114
|
try:
|
|
1115
1115
|
robot.add_sensor(sensor)
|
|
1116
|
-
except
|
|
1116
|
+
except (RobotModelError, ValueError) as e:
|
|
1117
1117
|
logger.warning(f"Skipping invalid sensor '{sensor.name}': {e}")
|
|
1118
1118
|
|
|
1119
1119
|
for gazebo_elem, physics_data in delayed_gazebo_elements:
|
|
@@ -1139,7 +1139,7 @@ class URDFParser(RobotXMLParser[Robot]):
|
|
|
1139
1139
|
or gazebo_elem.implicit_spring_damper is not None
|
|
1140
1140
|
):
|
|
1141
1141
|
robot.add_gazebo_element(gazebo_elem)
|
|
1142
|
-
except
|
|
1142
|
+
except (RobotModelError, ValueError) as e:
|
|
1143
1143
|
logger.warning(f"Skipping invalid gazebo element '{gazebo_elem.reference}': {e}")
|
|
1144
1144
|
|
|
1145
1145
|
return robot
|
|
@@ -6,9 +6,11 @@ properties, and includes.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import ast
|
|
9
10
|
import copy
|
|
10
11
|
import json
|
|
11
12
|
import math
|
|
13
|
+
import operator
|
|
12
14
|
import os
|
|
13
15
|
import re
|
|
14
16
|
import sys
|
|
@@ -48,8 +50,6 @@ logger = get_logger(__name__)
|
|
|
48
50
|
DEFAULT_MAX_DEPTH = 2000 # Increased for extremely complex industrial robots
|
|
49
51
|
RECURSION_LIMIT_BOOST = 5000 # Safer limit that prevents C-stack segmentation faults
|
|
50
52
|
|
|
51
|
-
_DUNDER_PATTERN: re.Pattern[str] = re.compile(r"__\w+__")
|
|
52
|
-
|
|
53
53
|
# Safe math context for evaluations
|
|
54
54
|
MATH_CONTEXT: dict[str, Any] = {
|
|
55
55
|
name: getattr(math, name) for name in dir(math) if not name.startswith("__")
|
|
@@ -72,6 +72,118 @@ MATH_CONTEXT["__builtins__"] = {
|
|
|
72
72
|
MATH_CONTEXT["true"] = True
|
|
73
73
|
MATH_CONTEXT["false"] = False
|
|
74
74
|
|
|
75
|
+
_SAFE_OPERATORS: dict[type[ast.AST], Any] = {
|
|
76
|
+
ast.Add: operator.add,
|
|
77
|
+
ast.Sub: operator.sub,
|
|
78
|
+
ast.Mult: operator.mul,
|
|
79
|
+
ast.Div: operator.truediv,
|
|
80
|
+
ast.FloorDiv: operator.floordiv,
|
|
81
|
+
ast.Mod: operator.mod,
|
|
82
|
+
ast.Pow: operator.pow,
|
|
83
|
+
ast.BitXor: operator.xor,
|
|
84
|
+
ast.BitOr: operator.or_,
|
|
85
|
+
ast.BitAnd: operator.and_,
|
|
86
|
+
ast.USub: operator.neg,
|
|
87
|
+
ast.UAdd: operator.pos,
|
|
88
|
+
ast.Not: operator.not_,
|
|
89
|
+
ast.Eq: operator.eq,
|
|
90
|
+
ast.NotEq: operator.ne,
|
|
91
|
+
ast.Lt: operator.lt,
|
|
92
|
+
ast.LtE: operator.le,
|
|
93
|
+
ast.Gt: operator.gt,
|
|
94
|
+
ast.GtE: operator.ge,
|
|
95
|
+
ast.And: lambda a, b: a and b,
|
|
96
|
+
ast.Or: lambda a, b: a or b,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _safe_eval(expr: str, context: dict[str, Any]) -> Any:
|
|
101
|
+
"""Safe evaluation of an AST expression using a restricted context."""
|
|
102
|
+
|
|
103
|
+
def _eval_node(node: ast.AST) -> Any:
|
|
104
|
+
if isinstance(node, ast.Expression):
|
|
105
|
+
return _eval_node(node.body)
|
|
106
|
+
elif isinstance(node, ast.Constant):
|
|
107
|
+
return node.value
|
|
108
|
+
elif isinstance(node, ast.Name):
|
|
109
|
+
if node.id.startswith("__") and node.id.endswith("__"):
|
|
110
|
+
raise RobotXacroExpressionError(expr, f"Forbidden dunder attributes: '{node.id}'")
|
|
111
|
+
if node.id in context:
|
|
112
|
+
return context[node.id]
|
|
113
|
+
if "__builtins__" in context and node.id in context["__builtins__"]:
|
|
114
|
+
return context["__builtins__"][node.id]
|
|
115
|
+
raise NameError(f"Unknown variable '{node.id}'") # noqa: TRY003
|
|
116
|
+
elif isinstance(node, ast.BinOp):
|
|
117
|
+
op = _SAFE_OPERATORS.get(type(node.op))
|
|
118
|
+
if not op:
|
|
119
|
+
raise TypeError(f"Unsupported operator {type(node.op)}") # noqa: TRY003
|
|
120
|
+
return op(_eval_node(node.left), _eval_node(node.right))
|
|
121
|
+
elif isinstance(node, ast.UnaryOp):
|
|
122
|
+
op = _SAFE_OPERATORS.get(type(node.op))
|
|
123
|
+
if not op:
|
|
124
|
+
raise TypeError(f"Unsupported unary operator {type(node.op)}") # noqa: TRY003
|
|
125
|
+
return op(_eval_node(node.operand))
|
|
126
|
+
elif isinstance(node, ast.BoolOp):
|
|
127
|
+
if isinstance(node.op, ast.And):
|
|
128
|
+
res = _eval_node(node.values[0])
|
|
129
|
+
for val in node.values[1:]:
|
|
130
|
+
res = res and _eval_node(val)
|
|
131
|
+
return res
|
|
132
|
+
elif isinstance(node.op, ast.Or):
|
|
133
|
+
res = _eval_node(node.values[0])
|
|
134
|
+
for val in node.values[1:]:
|
|
135
|
+
res = res or _eval_node(val)
|
|
136
|
+
return res
|
|
137
|
+
raise TypeError(f"Unsupported boolean operator {type(node.op)}") # noqa: TRY003
|
|
138
|
+
elif isinstance(node, ast.Compare):
|
|
139
|
+
left = _eval_node(node.left)
|
|
140
|
+
for cmp_op, right_node in zip(node.ops, node.comparators, strict=False):
|
|
141
|
+
op_func = _SAFE_OPERATORS.get(type(cmp_op))
|
|
142
|
+
if not op_func:
|
|
143
|
+
raise TypeError(f"Unsupported comparison operator {type(cmp_op)}") # noqa: TRY003
|
|
144
|
+
right = _eval_node(right_node)
|
|
145
|
+
if not op_func(left, right):
|
|
146
|
+
return False
|
|
147
|
+
left = right
|
|
148
|
+
return True
|
|
149
|
+
elif isinstance(node, ast.Call):
|
|
150
|
+
func = _eval_node(node.func)
|
|
151
|
+
if not callable(func):
|
|
152
|
+
raise TypeError(f"'{type(func)}' object is not callable") # noqa: TRY003
|
|
153
|
+
args = [_eval_node(arg) for arg in node.args]
|
|
154
|
+
kwargs = {kw.arg: _eval_node(kw.value) for kw in node.keywords if kw.arg is not None}
|
|
155
|
+
return func(*args, **kwargs)
|
|
156
|
+
elif isinstance(node, ast.Attribute):
|
|
157
|
+
if node.attr.startswith("__") and node.attr.endswith("__"):
|
|
158
|
+
raise RobotXacroExpressionError(expr, f"Forbidden dunder attributes: '{node.attr}'")
|
|
159
|
+
val = _eval_node(node.value)
|
|
160
|
+
if hasattr(val, node.attr):
|
|
161
|
+
return getattr(val, node.attr)
|
|
162
|
+
elif isinstance(val, dict) and node.attr in val:
|
|
163
|
+
return val[node.attr]
|
|
164
|
+
raise AttributeError(f"Object has no attribute '{node.attr}'") # noqa: TRY003
|
|
165
|
+
elif isinstance(node, ast.Subscript):
|
|
166
|
+
val = _eval_node(node.value)
|
|
167
|
+
slice_val = _eval_node(node.slice)
|
|
168
|
+
return val[slice_val]
|
|
169
|
+
elif isinstance(node, ast.List):
|
|
170
|
+
return [_eval_node(elt) for elt in node.elts]
|
|
171
|
+
elif isinstance(node, ast.Dict):
|
|
172
|
+
return {
|
|
173
|
+
_eval_node(k) if k else None: _eval_node(v)
|
|
174
|
+
for k, v in zip(node.keys, node.values, strict=False)
|
|
175
|
+
}
|
|
176
|
+
elif isinstance(node, ast.Tuple):
|
|
177
|
+
return tuple(_eval_node(elt) for elt in node.elts)
|
|
178
|
+
else:
|
|
179
|
+
raise TypeError(f"Unsupported AST node {type(node)}") # noqa: TRY003
|
|
180
|
+
|
|
181
|
+
# We no longer catch SyntaxError and wrap it, allowing it to bubble up
|
|
182
|
+
# just like eval() did, so the existing try/except blocks work as before.
|
|
183
|
+
tree = ast.parse(expr, mode="eval")
|
|
184
|
+
return _eval_node(tree)
|
|
185
|
+
|
|
186
|
+
|
|
75
187
|
# Internal XML tags used for structural processing
|
|
76
188
|
_TAG_CONTAINER = "container"
|
|
77
189
|
_TAG_SKIP = "skip"
|
|
@@ -733,10 +845,8 @@ class XacroResolver:
|
|
|
733
845
|
else:
|
|
734
846
|
try:
|
|
735
847
|
# Standard expression evaluation
|
|
736
|
-
if _DUNDER_PATTERN.search(condition_str):
|
|
737
|
-
raise RobotXacroExpressionError(condition_str, "Forbidden dunder attributes")
|
|
738
848
|
ctx = {**self.eval_context, **self.properties, **self.args}
|
|
739
|
-
return bool(
|
|
849
|
+
return bool(_safe_eval(condition_str, ctx))
|
|
740
850
|
except Exception as e:
|
|
741
851
|
if isinstance(e, RobotParserError):
|
|
742
852
|
raise
|
|
@@ -894,9 +1004,6 @@ class XacroResolver:
|
|
|
894
1004
|
Raises:
|
|
895
1005
|
RobotXacroExpressionError: If the expression is invalid or contains forbidden dunder attributes.
|
|
896
1006
|
"""
|
|
897
|
-
if _DUNDER_PATTERN.search(expr):
|
|
898
|
-
raise RobotXacroExpressionError(expr, "Forbidden dunder attributes")
|
|
899
|
-
|
|
900
1007
|
try:
|
|
901
1008
|
# Build nested context for hierarchical namespaces (e.g. arm.mass)
|
|
902
1009
|
ctx = self.eval_context.copy()
|
|
@@ -925,7 +1032,7 @@ class XacroResolver:
|
|
|
925
1032
|
if "." not in short_name:
|
|
926
1033
|
ctx[short_name] = val
|
|
927
1034
|
|
|
928
|
-
return
|
|
1035
|
+
return _safe_eval(expr, ctx)
|
|
929
1036
|
except Exception as e:
|
|
930
1037
|
# CRITICAL: Do not silent-fail! If math fails (e.g. missing variable),
|
|
931
1038
|
# we must tell the user immediately rather than producing a corrupt output.
|
|
@@ -317,8 +317,8 @@ class RobotXMLParser(RobotParser[T], Generic[T]):
|
|
|
317
317
|
inertia = InertiaTensor(ixx=ixx, iyy=iyy, izz=izz, ixy=ixy, ixz=ixz, iyz=iyz)
|
|
318
318
|
except RobotModelError:
|
|
319
319
|
# If triangle inequality is still violated, fall back to minimal valid
|
|
320
|
-
inertia = InertiaTensor.
|
|
320
|
+
inertia = InertiaTensor.stability_floor()
|
|
321
321
|
else:
|
|
322
|
-
inertia = InertiaTensor.
|
|
322
|
+
inertia = InertiaTensor.stability_floor()
|
|
323
323
|
|
|
324
324
|
return Inertial(mass=mass, origin=origin, inertia=inertia)
|
|
@@ -43,9 +43,11 @@ def validate_mesh_path(
|
|
|
43
43
|
RobotSecurityError: If the mesh path attempts to escape the source directory
|
|
44
44
|
RobotSecurityError: If absolute paths are not allowed but one is provided
|
|
45
45
|
"""
|
|
46
|
-
# Decode URL encoding to catch encoded path traversal attempts (e.g., %
|
|
46
|
+
# Decode URL encoding to catch encoded path traversal attempts (e.g., %252e%252e%252f -> ../)
|
|
47
47
|
mesh_str = str(mesh_filepath)
|
|
48
48
|
decoded_str = unquote(mesh_str)
|
|
49
|
+
while unquote(decoded_str) != decoded_str:
|
|
50
|
+
decoded_str = unquote(decoded_str)
|
|
49
51
|
|
|
50
52
|
# Recreate Path from decoded string for further validation
|
|
51
53
|
if decoded_str != mesh_str:
|
|
@@ -122,6 +124,8 @@ def validate_package_uri(uri: str) -> str:
|
|
|
122
124
|
|
|
123
125
|
# Decode URL encoding to catch encoded path traversal attempts
|
|
124
126
|
decoded_uri = unquote(uri)
|
|
127
|
+
while unquote(decoded_uri) != decoded_uri:
|
|
128
|
+
decoded_uri = unquote(decoded_uri)
|
|
125
129
|
|
|
126
130
|
# Check both original and decoded for path traversal
|
|
127
131
|
if ".." in uri or ".." in decoded_uri:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -230,9 +230,6 @@ class RobotBuilder:
|
|
|
230
230
|
self._active_link_builders.clear()
|
|
231
231
|
|
|
232
232
|
if validate:
|
|
233
|
-
# Trigger root search to verify connectivity (raises error if no root)
|
|
234
|
-
_ = self.robot.root_link
|
|
235
|
-
|
|
236
233
|
if self.robot.has_cycle:
|
|
237
234
|
raise RobotValidationError(
|
|
238
235
|
ValidationErrorCode.HAS_CYCLE,
|
|
@@ -240,6 +237,9 @@ class RobotBuilder:
|
|
|
240
237
|
target="KinematicTree",
|
|
241
238
|
)
|
|
242
239
|
|
|
240
|
+
# Trigger root search to verify connectivity (raises error if no root)
|
|
241
|
+
_ = self.robot.root_link
|
|
242
|
+
|
|
243
243
|
return self.robot
|
|
244
244
|
|
|
245
245
|
def export_urdf(self, validate: bool = True, pretty_print: bool = True) -> str:
|
|
File without changes
|
{linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/srdf_generator.py
RENAMED
|
File without changes
|
{linkforge_core-1.4.0 → linkforge_core-1.4.2}/src/linkforge/core/generators/xacro_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|