cyberwave-robot-format 0.1.2__py3-none-any.whl
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.
- cyberwave_robot_format/__init__.py +80 -0
- cyberwave_robot_format/core.py +370 -0
- cyberwave_robot_format/math_utils.py +110 -0
- cyberwave_robot_format/mesh/__init__.py +13 -0
- cyberwave_robot_format/mesh/processing.py +169 -0
- cyberwave_robot_format/mesh/resolution.py +162 -0
- cyberwave_robot_format/mjcf/__init__.py +10 -0
- cyberwave_robot_format/mjcf/exporter.py +786 -0
- cyberwave_robot_format/mjcf/parser.py +922 -0
- cyberwave_robot_format/mjcf/scene_export.py +269 -0
- cyberwave_robot_format/mjcf/utils.py +98 -0
- cyberwave_robot_format/schema.py +816 -0
- cyberwave_robot_format/schema_export.py +62 -0
- cyberwave_robot_format/serialization.py +139 -0
- cyberwave_robot_format/urdf/__init__.py +7 -0
- cyberwave_robot_format/urdf/exporter.py +233 -0
- cyberwave_robot_format/urdf/parser.py +765 -0
- cyberwave_robot_format/urdf/scene_export.py +261 -0
- cyberwave_robot_format/utils.py +358 -0
- cyberwave_robot_format-0.1.2.dist-info/METADATA +163 -0
- cyberwave_robot_format-0.1.2.dist-info/RECORD +25 -0
- cyberwave_robot_format-0.1.2.dist-info/WHEEL +5 -0
- cyberwave_robot_format-0.1.2.dist-info/licenses/LICENSE +201 -0
- cyberwave_robot_format-0.1.2.dist-info/licenses/NOTICE +7 -0
- cyberwave_robot_format-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Cyberwave Robot Format - Universal robot description schema and format converters.
|
|
2
|
+
|
|
3
|
+
This package provides a unified schema for representing robotic assets and tools
|
|
4
|
+
for converting between different robot description formats (URDF, MJCF, SDF, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from cyberwave_robot_format.schema import (
|
|
8
|
+
Actuator,
|
|
9
|
+
ActuatorType,
|
|
10
|
+
Collision,
|
|
11
|
+
CollisionConfig,
|
|
12
|
+
CollisionExclude,
|
|
13
|
+
CollisionGroup,
|
|
14
|
+
CollisionPair,
|
|
15
|
+
CommonSchema,
|
|
16
|
+
Contact,
|
|
17
|
+
ContactSurface,
|
|
18
|
+
Geometry,
|
|
19
|
+
GeometryType,
|
|
20
|
+
Inertia,
|
|
21
|
+
Joint,
|
|
22
|
+
JointDynamics,
|
|
23
|
+
JointLimits,
|
|
24
|
+
JointType,
|
|
25
|
+
Link,
|
|
26
|
+
Material,
|
|
27
|
+
Metadata,
|
|
28
|
+
Physics,
|
|
29
|
+
PhysicsSolver,
|
|
30
|
+
Pose,
|
|
31
|
+
Quaternion,
|
|
32
|
+
Scene,
|
|
33
|
+
Sensor,
|
|
34
|
+
Vector3,
|
|
35
|
+
Visual,
|
|
36
|
+
)
|
|
37
|
+
from cyberwave_robot_format.urdf import URDFParser, URDFExporter, export_urdf_zip_cloud, export_urdf_scene_xml
|
|
38
|
+
from cyberwave_robot_format.mjcf import MJCFParser, MJCFExporter, export_mujoco_zip_cloud, export_mujoco_scene_xml
|
|
39
|
+
from cyberwave_robot_format.schema_export import export_universal_schema_json
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
__all__ = [
|
|
43
|
+
"CommonSchema",
|
|
44
|
+
"Link",
|
|
45
|
+
"Joint",
|
|
46
|
+
"Visual",
|
|
47
|
+
"Collision",
|
|
48
|
+
"CollisionConfig",
|
|
49
|
+
"CollisionExclude",
|
|
50
|
+
"CollisionGroup",
|
|
51
|
+
"CollisionPair",
|
|
52
|
+
"Geometry",
|
|
53
|
+
"Material",
|
|
54
|
+
"Pose",
|
|
55
|
+
"Vector3",
|
|
56
|
+
"Quaternion",
|
|
57
|
+
"Inertia",
|
|
58
|
+
"JointType",
|
|
59
|
+
"GeometryType",
|
|
60
|
+
"JointLimits",
|
|
61
|
+
"JointDynamics",
|
|
62
|
+
"Actuator",
|
|
63
|
+
"ActuatorType",
|
|
64
|
+
"Sensor",
|
|
65
|
+
"Contact",
|
|
66
|
+
"ContactSurface",
|
|
67
|
+
"Physics",
|
|
68
|
+
"PhysicsSolver",
|
|
69
|
+
"Scene",
|
|
70
|
+
"Metadata",
|
|
71
|
+
"URDFParser",
|
|
72
|
+
"URDFExporter",
|
|
73
|
+
"MJCFParser",
|
|
74
|
+
"MJCFExporter",
|
|
75
|
+
"export_mujoco_zip_cloud",
|
|
76
|
+
"export_mujoco_scene_xml",
|
|
77
|
+
"export_urdf_zip_cloud",
|
|
78
|
+
"export_urdf_scene_xml",
|
|
79
|
+
"export_universal_schema_json",
|
|
80
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core format conversion engine and orchestration classes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .schema import CommonSchema, Material
|
|
13
|
+
from .utils import detect_format, validate_schema
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ParseError(Exception):
|
|
19
|
+
"""Exception raised during parsing errors."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(Exception):
|
|
25
|
+
"""Exception raised during validation errors."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ParseContext:
|
|
32
|
+
"""Context information for parsing operations with enhanced error tracking."""
|
|
33
|
+
|
|
34
|
+
file_path: Path
|
|
35
|
+
base_dir: Path
|
|
36
|
+
materials: dict[str, Material] = field(default_factory=dict)
|
|
37
|
+
meshes: dict[str, str] = field(default_factory=dict)
|
|
38
|
+
warnings: list[str] = field(default_factory=list)
|
|
39
|
+
errors: list[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
def add_warning(self, message: str, element: str | None = None) -> None:
|
|
42
|
+
"""Add warning message with optional element context."""
|
|
43
|
+
if element:
|
|
44
|
+
message = f"{element}: {message}"
|
|
45
|
+
self.warnings.append(message)
|
|
46
|
+
logger.warning(message)
|
|
47
|
+
|
|
48
|
+
def add_error(self, message: str, element: str | None = None) -> None:
|
|
49
|
+
"""Add error message with optional element context."""
|
|
50
|
+
if element:
|
|
51
|
+
message = f"{element}: {message}"
|
|
52
|
+
self.errors.append(message)
|
|
53
|
+
logger.error(message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BaseParser(ABC):
|
|
57
|
+
"""Base class for format parsers."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def parse(self, input_path: str | Path) -> CommonSchema:
|
|
61
|
+
"""Parse input file and return common schema representation."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def can_parse(self, file_path: str | Path) -> bool:
|
|
65
|
+
"""Check if this parser can handle the given file."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BaseExporter(ABC):
|
|
69
|
+
"""Base class for format exporters."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def export(self, schema: CommonSchema, output_path: str | Path) -> None:
|
|
73
|
+
"""Export common schema to target format."""
|
|
74
|
+
|
|
75
|
+
def get_extension(self) -> str:
|
|
76
|
+
"""Return the file extension for this format."""
|
|
77
|
+
raise NotImplementedError("Subclasses must implement get_extension()")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConversionEngine:
|
|
81
|
+
"""Core conversion engine that orchestrates format conversions.
|
|
82
|
+
|
|
83
|
+
The engine maintains a registry of parsers and exporters for different
|
|
84
|
+
formats and handles the conversion workflow between them via the common
|
|
85
|
+
schema intermediate representation.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> engine = ConversionEngine()
|
|
89
|
+
>>> engine.register_parser('urdf', URDFParser())
|
|
90
|
+
>>> engine.register_exporter('sdf', SDFExporter())
|
|
91
|
+
>>> engine.convert('robot.urdf', 'robot.sdf')
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self):
|
|
95
|
+
self.parsers: dict[str, BaseParser] = {}
|
|
96
|
+
self.exporters: dict[str, BaseExporter] = {}
|
|
97
|
+
|
|
98
|
+
def register_parser(self, format_name: str, parser: BaseParser) -> None:
|
|
99
|
+
"""Register a parser for a specific format."""
|
|
100
|
+
self.parsers[format_name.lower()] = parser
|
|
101
|
+
logger.debug("Registered parser for format: %s.", format_name)
|
|
102
|
+
|
|
103
|
+
def register_exporter(self, format_name: str, exporter: BaseExporter) -> None:
|
|
104
|
+
"""Register an exporter for a specific format."""
|
|
105
|
+
self.exporters[format_name.lower()] = exporter
|
|
106
|
+
logger.debug("Registered exporter for format: %s.", format_name)
|
|
107
|
+
|
|
108
|
+
def get_parser(self, format_name: str) -> BaseParser | None:
|
|
109
|
+
"""Get parser for a specific format."""
|
|
110
|
+
return self.parsers.get(format_name.lower())
|
|
111
|
+
|
|
112
|
+
def get_exporter(self, format_name: str) -> BaseExporter | None:
|
|
113
|
+
"""Get exporter for a specific format."""
|
|
114
|
+
return self.exporters.get(format_name.lower())
|
|
115
|
+
|
|
116
|
+
def detect_format(self, file_path: str | Path) -> str | None:
|
|
117
|
+
"""Detect format of a file using registered parsers."""
|
|
118
|
+
file_path = Path(file_path)
|
|
119
|
+
|
|
120
|
+
# Try each parser to see if it can parse the file
|
|
121
|
+
for format_name, parser in self.parsers.items():
|
|
122
|
+
if parser.can_parse(file_path):
|
|
123
|
+
return format_name
|
|
124
|
+
|
|
125
|
+
# Fall back to utility function
|
|
126
|
+
return detect_format(file_path)
|
|
127
|
+
|
|
128
|
+
def get_supported_formats(self) -> dict[str, list[str]]:
|
|
129
|
+
"""Return supported formats for parsing and exporting."""
|
|
130
|
+
return {
|
|
131
|
+
"parsers": list(self.parsers.keys()),
|
|
132
|
+
"exporters": list(self.exporters.keys()),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def convert(
|
|
136
|
+
self,
|
|
137
|
+
input_path: str | Path,
|
|
138
|
+
output_path: str | Path,
|
|
139
|
+
source_format: str | None = None,
|
|
140
|
+
target_format: str | None = None,
|
|
141
|
+
validation: bool = True,
|
|
142
|
+
) -> CommonSchema:
|
|
143
|
+
"""Convert between robot description formats.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
input_path: Path to input file
|
|
147
|
+
output_path: Path to output file
|
|
148
|
+
source_format: Source format (auto-detected if None)
|
|
149
|
+
target_format: Target format (inferred from extension if None)
|
|
150
|
+
validation: Whether to validate schema during conversion
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
CommonSchema representation of the robot model.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: If formats are unsupported or conversion fails.
|
|
157
|
+
FileNotFoundError: If input file doesn't exist.
|
|
158
|
+
"""
|
|
159
|
+
input_path = Path(input_path)
|
|
160
|
+
output_path = Path(output_path)
|
|
161
|
+
|
|
162
|
+
if not input_path.exists():
|
|
163
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
164
|
+
|
|
165
|
+
# Auto-detect source format if not specified
|
|
166
|
+
if source_format is None:
|
|
167
|
+
source_format = self.detect_format(input_path)
|
|
168
|
+
if source_format is None:
|
|
169
|
+
raise ValueError(f"Cannot detect format for: {input_path}")
|
|
170
|
+
|
|
171
|
+
# Infer target format from extension if not specified
|
|
172
|
+
if target_format is None:
|
|
173
|
+
target_format = output_path.suffix.lstrip(".").lower()
|
|
174
|
+
|
|
175
|
+
# Get appropriate parser and exporter
|
|
176
|
+
parser = self.parsers.get(source_format.lower())
|
|
177
|
+
if parser is None:
|
|
178
|
+
raise ValueError(f"No parser found for format: {source_format}")
|
|
179
|
+
|
|
180
|
+
exporter = self.exporters.get(target_format.lower())
|
|
181
|
+
if exporter is None:
|
|
182
|
+
raise ValueError(f"No exporter found for format: {target_format}")
|
|
183
|
+
|
|
184
|
+
# Parse input to common schema
|
|
185
|
+
logger.info("Parsing %s file: %s.", source_format.upper(), input_path)
|
|
186
|
+
schema = parser.parse(input_path)
|
|
187
|
+
|
|
188
|
+
# Validate schema if requested
|
|
189
|
+
if validation:
|
|
190
|
+
logger.debug("Validating intermediate schema.")
|
|
191
|
+
validate_schema(schema)
|
|
192
|
+
|
|
193
|
+
# Export to target format
|
|
194
|
+
logger.info("Exporting to %s: %s.", target_format.upper(), output_path)
|
|
195
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
exporter.export(schema, output_path)
|
|
197
|
+
|
|
198
|
+
logger.info("Conversion complete: %s -> %s.", input_path, output_path)
|
|
199
|
+
return schema
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class FormatConverter:
|
|
203
|
+
"""High-level interface for robot format conversions.
|
|
204
|
+
|
|
205
|
+
This class provides a simplified API for common conversion tasks,
|
|
206
|
+
with built-in parsers and exporters for standard formats.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> converter = FormatConverter()
|
|
210
|
+
>>> converter.urdf_to_sdf('robot.urdf', 'robot.sdf')
|
|
211
|
+
>>> converter.batch_convert('models/', 'output/', 'urdf', 'mjcf')
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self.engine = ConversionEngine()
|
|
216
|
+
self._register_default_processors()
|
|
217
|
+
|
|
218
|
+
def _register_default_processors(self) -> None:
|
|
219
|
+
"""Register default parsers and exporters for standard formats."""
|
|
220
|
+
# Import from submodules
|
|
221
|
+
from .mjcf import MJCFExporter, MJCFParser
|
|
222
|
+
from .obj import OBJMTLExporter, OBJMTLParser
|
|
223
|
+
from .schema_io import SchemaExporter, SchemaParser
|
|
224
|
+
from .sdf import SDFExporter, SDFParser
|
|
225
|
+
from .urdf import URDFExporter, URDFParser
|
|
226
|
+
|
|
227
|
+
# Register parsers
|
|
228
|
+
self.engine.register_parser("urdf", URDFParser())
|
|
229
|
+
self.engine.register_parser("sdf", SDFParser())
|
|
230
|
+
self.engine.register_parser("mjcf", MJCFParser())
|
|
231
|
+
self.engine.register_parser("xml", MJCFParser()) # MJCF is XML
|
|
232
|
+
self.engine.register_parser("schema", SchemaParser())
|
|
233
|
+
self.engine.register_parser("yaml", SchemaParser())
|
|
234
|
+
self.engine.register_parser("json", SchemaParser())
|
|
235
|
+
self.engine.register_parser("obj", OBJMTLParser())
|
|
236
|
+
|
|
237
|
+
# Register exporters
|
|
238
|
+
self.engine.register_exporter("urdf", URDFExporter())
|
|
239
|
+
self.engine.register_exporter("sdf", SDFExporter())
|
|
240
|
+
self.engine.register_exporter("mjcf", MJCFExporter())
|
|
241
|
+
self.engine.register_exporter("xml", MJCFExporter())
|
|
242
|
+
self.engine.register_exporter("schema", SchemaExporter())
|
|
243
|
+
self.engine.register_exporter("yaml", SchemaExporter())
|
|
244
|
+
self.engine.register_exporter("json", SchemaExporter())
|
|
245
|
+
self.engine.register_exporter("obj", OBJMTLExporter())
|
|
246
|
+
|
|
247
|
+
# Try to register USD support if available
|
|
248
|
+
try:
|
|
249
|
+
from .usd import USDExporter, USDParser
|
|
250
|
+
|
|
251
|
+
self.engine.register_parser("usd", USDParser())
|
|
252
|
+
self.engine.register_parser("usda", USDParser())
|
|
253
|
+
self.engine.register_exporter("usd", USDExporter())
|
|
254
|
+
self.engine.register_exporter("usda", USDExporter())
|
|
255
|
+
except ImportError:
|
|
256
|
+
logger.debug("USD support not available (missing pxr module).")
|
|
257
|
+
|
|
258
|
+
def convert(
|
|
259
|
+
self, input_path: str | Path, output_path: str | Path, **kwargs
|
|
260
|
+
) -> CommonSchema:
|
|
261
|
+
"""Convert between formats using the conversion engine."""
|
|
262
|
+
return self.engine.convert(input_path, output_path, **kwargs)
|
|
263
|
+
|
|
264
|
+
def urdf_to_sdf(self, urdf_path: str, sdf_path: str) -> CommonSchema:
|
|
265
|
+
"""Convert URDF to SDF format."""
|
|
266
|
+
return self.convert(
|
|
267
|
+
urdf_path, sdf_path, source_format="urdf", target_format="sdf"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def urdf_to_mjcf(self, urdf_path: str, mjcf_path: str) -> CommonSchema:
|
|
271
|
+
"""Convert URDF to MJCF format."""
|
|
272
|
+
return self.convert(
|
|
273
|
+
urdf_path, mjcf_path, source_format="urdf", target_format="mjcf"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def sdf_to_urdf(self, sdf_path: str, urdf_path: str) -> CommonSchema:
|
|
277
|
+
"""Convert SDF to URDF format."""
|
|
278
|
+
return self.convert(
|
|
279
|
+
sdf_path, urdf_path, source_format="sdf", target_format="urdf"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def to_schema(self, input_path: str, schema_path: str) -> CommonSchema:
|
|
283
|
+
"""Convert any supported format to common schema."""
|
|
284
|
+
return self.convert(input_path, schema_path, target_format="schema")
|
|
285
|
+
|
|
286
|
+
def from_schema(
|
|
287
|
+
self, schema_path: str, output_path: str, target_format: str | None = None
|
|
288
|
+
) -> CommonSchema:
|
|
289
|
+
"""Convert from common schema to any supported format."""
|
|
290
|
+
return self.convert(
|
|
291
|
+
schema_path,
|
|
292
|
+
output_path,
|
|
293
|
+
source_format="schema",
|
|
294
|
+
target_format=target_format,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def batch_convert(
|
|
298
|
+
self,
|
|
299
|
+
input_dir: str | Path,
|
|
300
|
+
output_dir: str | Path,
|
|
301
|
+
source_format: str,
|
|
302
|
+
target_format: str,
|
|
303
|
+
pattern: str = "*",
|
|
304
|
+
) -> list[Path]:
|
|
305
|
+
"""Batch convert files in a directory.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
input_dir: Input directory path
|
|
309
|
+
output_dir: Output directory path
|
|
310
|
+
source_format: Source file format
|
|
311
|
+
target_format: Target file format
|
|
312
|
+
pattern: File pattern to match (default: "*")
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of successfully converted output files.
|
|
316
|
+
"""
|
|
317
|
+
input_dir = Path(input_dir)
|
|
318
|
+
output_dir = Path(output_dir)
|
|
319
|
+
|
|
320
|
+
if not input_dir.exists():
|
|
321
|
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
|
322
|
+
|
|
323
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
|
|
325
|
+
# Find matching files
|
|
326
|
+
# If pattern already includes extension, use it directly
|
|
327
|
+
# Otherwise, append the source format extension
|
|
328
|
+
if pattern.endswith(f".{source_format}") or "*" in pattern:
|
|
329
|
+
# Pattern already specifies the extension
|
|
330
|
+
input_files = list(input_dir.glob(pattern))
|
|
331
|
+
else:
|
|
332
|
+
ext = f".{source_format}"
|
|
333
|
+
input_files = list(input_dir.glob(f"{pattern}{ext}"))
|
|
334
|
+
|
|
335
|
+
if not input_files:
|
|
336
|
+
logger.warning("No %s files found in %s.", source_format, input_dir)
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
converted_files = []
|
|
340
|
+
target_ext = f".{target_format}"
|
|
341
|
+
|
|
342
|
+
for input_file in input_files:
|
|
343
|
+
output_file = output_dir / (input_file.stem + target_ext)
|
|
344
|
+
try:
|
|
345
|
+
self.convert(
|
|
346
|
+
input_file,
|
|
347
|
+
output_file,
|
|
348
|
+
source_format=source_format,
|
|
349
|
+
target_format=target_format,
|
|
350
|
+
)
|
|
351
|
+
converted_files.append(output_file)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error("Failed to convert %s: %s.", input_file, e)
|
|
354
|
+
|
|
355
|
+
logger.info(
|
|
356
|
+
"Batch conversion complete: %s/%s files converted.",
|
|
357
|
+
len(converted_files),
|
|
358
|
+
len(input_files),
|
|
359
|
+
)
|
|
360
|
+
return converted_files
|
|
361
|
+
|
|
362
|
+
def get_conversion_matrix(self) -> dict[str, list[str]]:
|
|
363
|
+
"""Get matrix of supported conversion paths."""
|
|
364
|
+
formats = self.engine.get_supported_formats()
|
|
365
|
+
matrix = {}
|
|
366
|
+
|
|
367
|
+
for source_fmt in formats["parsers"]:
|
|
368
|
+
matrix[source_fmt] = formats["exporters"]
|
|
369
|
+
|
|
370
|
+
return matrix
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Math utilities for robot format conversion.
|
|
3
|
+
|
|
4
|
+
This module provides common mathematical types used across the format conversion
|
|
5
|
+
pipeline: vectors, quaternions, poses, and inertia tensors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Vector3:
|
|
18
|
+
"""3D vector representation."""
|
|
19
|
+
|
|
20
|
+
x: float = 0.0
|
|
21
|
+
y: float = 0.0
|
|
22
|
+
z: float = 0.0
|
|
23
|
+
|
|
24
|
+
def to_list(self) -> list[float]:
|
|
25
|
+
"""Convert to list format."""
|
|
26
|
+
return [self.x, self.y, self.z]
|
|
27
|
+
|
|
28
|
+
def to_array(self) -> np.ndarray:
|
|
29
|
+
"""Convert to numpy array."""
|
|
30
|
+
return np.array([self.x, self.y, self.z])
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_list(cls, values: list[float]) -> Vector3:
|
|
34
|
+
"""Create from list of values."""
|
|
35
|
+
return cls(values[0], values[1], values[2])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Quaternion:
|
|
40
|
+
"""Quaternion representation for rotations."""
|
|
41
|
+
|
|
42
|
+
x: float = 0.0
|
|
43
|
+
y: float = 0.0
|
|
44
|
+
z: float = 0.0
|
|
45
|
+
w: float = 1.0
|
|
46
|
+
|
|
47
|
+
def to_list(self) -> list[float]:
|
|
48
|
+
"""Convert to list format [x, y, z, w]."""
|
|
49
|
+
return [self.x, self.y, self.z, self.w]
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_rpy(cls, roll: float, pitch: float, yaw: float) -> Quaternion:
|
|
53
|
+
"""Create quaternion from roll-pitch-yaw angles (Fixed XYZ convention)."""
|
|
54
|
+
cy = math.cos(yaw * 0.5)
|
|
55
|
+
sy = math.sin(yaw * 0.5)
|
|
56
|
+
cp = math.cos(pitch * 0.5)
|
|
57
|
+
sp = math.sin(pitch * 0.5)
|
|
58
|
+
cr = math.cos(roll * 0.5)
|
|
59
|
+
sr = math.sin(roll * 0.5)
|
|
60
|
+
|
|
61
|
+
w = cr * cp * cy + sr * sp * sy
|
|
62
|
+
x = sr * cp * cy - cr * sp * sy
|
|
63
|
+
y = cr * sp * cy + sr * cp * sy
|
|
64
|
+
z = cr * cp * sy - sr * sp * cy
|
|
65
|
+
|
|
66
|
+
return cls(x, y, z, w)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Pose:
|
|
71
|
+
"""6DOF pose representation."""
|
|
72
|
+
|
|
73
|
+
position: Vector3 = None # type: ignore[assignment]
|
|
74
|
+
orientation: Quaternion = None # type: ignore[assignment]
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
if self.position is None:
|
|
78
|
+
self.position = Vector3()
|
|
79
|
+
if self.orientation is None:
|
|
80
|
+
self.orientation = Quaternion()
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_xyzrpy(cls, xyz: list[float], rpy: list[float]) -> Pose:
|
|
84
|
+
"""Create pose from position and RPY orientation."""
|
|
85
|
+
return cls(
|
|
86
|
+
position=Vector3.from_list(xyz),
|
|
87
|
+
orientation=Quaternion.from_rpy(rpy[0], rpy[1], rpy[2]),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Inertia:
|
|
93
|
+
"""Inertia tensor representation."""
|
|
94
|
+
|
|
95
|
+
ixx: float = 0.0
|
|
96
|
+
iyy: float = 0.0
|
|
97
|
+
izz: float = 0.0
|
|
98
|
+
ixy: float = 0.0
|
|
99
|
+
ixz: float = 0.0
|
|
100
|
+
iyz: float = 0.0
|
|
101
|
+
|
|
102
|
+
def to_matrix(self) -> np.ndarray:
|
|
103
|
+
"""Convert to 3x3 inertia matrix."""
|
|
104
|
+
return np.array(
|
|
105
|
+
[
|
|
106
|
+
[self.ixx, self.ixy, self.ixz],
|
|
107
|
+
[self.ixy, self.iyy, self.iyz],
|
|
108
|
+
[self.ixz, self.iyz, self.izz],
|
|
109
|
+
]
|
|
110
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from cyberwave_robot_format.mesh.processing import (
|
|
2
|
+
convert_dae_to_obj,
|
|
3
|
+
convert_mesh_bytes_to_obj,
|
|
4
|
+
get_mesh_lookup_key,
|
|
5
|
+
)
|
|
6
|
+
from cyberwave_robot_format.mesh.resolution import resolve_mesh_uri
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"convert_dae_to_obj",
|
|
10
|
+
"convert_mesh_bytes_to_obj",
|
|
11
|
+
"get_mesh_lookup_key",
|
|
12
|
+
"resolve_mesh_uri",
|
|
13
|
+
]
|