Files
cadSet/text-to-cad/skills/sdf/references/generator-contract.md
T
2026-07-22 19:38:36 +08:00

152 lines
4.6 KiB
Markdown

# SDF generator contract
Use this reference when creating or editing Python sources that generate SDFormat/SDF files.
## Source of truth
The Python source that defines `gen_sdf()` is the source of truth. The configured `.sdf` output is generated and should not be hand-edited.
## Required shape
`gen_sdf()` must be a top-level zero-argument function. Prefer returning the root `xml.etree.ElementTree.Element` for a complete SDFormat document.
```python
import xml.etree.ElementTree as ET
def gen_sdf():
sdf = ET.Element("sdf", {"version": "1.12"})
model = ET.SubElement(sdf, "model", {"name": "sample_robot"})
ET.SubElement(model, "link", {"name": "base_link"})
return sdf
```
The root must be `<sdf>` and must include a non-empty `version` attribute. Default new outputs to `version="1.12"` unless the user or target simulator requires another version.
## Accepted return values
Preferred:
```python
def gen_sdf():
return sdf_root_element
```
Accepted for compatibility:
```python
def gen_sdf():
return """<?xml version="1.0"?>
<sdf version="1.12">...</sdf>
"""
```
Envelope form:
```python
def gen_sdf():
return {
"xml": sdf_root_element,
"assumptions": [
{"code": "mesh_units", "message": "Assumed mesh units are meters."},
"Assumed lidar frame is coincident with lidar_link.",
],
"warnings": [
{"code": "plugin_unverified", "message": "Camera plugin filename was not verified in the target simulator."}
],
"metadata": {
"target_consumer": "Gazebo Harmonic",
"sdf_version": "1.12",
},
}
```
Envelope rules:
- `xml` is required and may be an XML element or XML string.
- `assumptions`, when present, must be a list of strings or dicts with `code`, `message`, and optional `source`.
- `warnings`, when present, must be a list of strings or dicts with `code`, `message`, and optional `source`.
- `metadata`, when present, must be a JSON-serializable dict with scalar values.
- Output-path fields such as `sdf_output` are unsupported; select output paths with CLI targets.
- Unsupported envelope fields should fail with a clear error.
Use envelope assumptions to make spatial, physical, simulator, and resource assumptions auditable. Do not hide guesses in XML literals.
## Output path
The generated `.sdf` output path is selected by the CLI:
- plain source target: sibling `.sdf` beside the source;
- `-o` / `--output`: override for one plain target;
- `SOURCE.py=OUTPUT.sdf`: per-target override for multi-target generation.
The generator should not write the output file itself.
## Source-relative asset paths
Do not rely on the shell working directory for mesh or resource paths inside generator code. Prefer source-relative constants:
```python
from pathlib import Path
SOURCE_DIR = Path(__file__).resolve().parent
MESH_DIR = SOURCE_DIR / "meshes"
```
For model packages, prefer URI conventions understood by the target simulator, such as `model://...`, `package://...`, or stable relative paths from the generated `.sdf` location.
## Pose and frame discipline
Before emitting `<pose>`, `<frame>`, `<joint>`, `<axis>`, `<visual>`, `<collision>`, sensor, or plugin placement elements, fill out the design ledger:
- pose value;
- pose frame or `relative_to`;
- joint axis and `expressed_in` frame;
- positive-motion convention;
- mesh units and scale;
- source of every value.
Use constants with names that expose assumptions:
```python
ASSUMED_BASE_TO_LIDAR_Z_M = 0.18
ASSUMED_LIDAR_YAW_RAD = 0.0
```
Do not hide spatial guesses inside XML literals.
## Minimal model example with explicit pose intent
```python
import xml.etree.ElementTree as ET
def text(parent, tag, value, attrib=None):
child = ET.SubElement(parent, tag, attrib or {})
child.text = str(value)
return child
def pose(parent, xyz=(0, 0, 0), rpy=(0, 0, 0), *, relative_to=None):
attrib = {"relative_to": relative_to} if relative_to else {}
return text(parent, "pose", " ".join(str(v) for v in (*xyz, *rpy)), attrib)
def gen_sdf():
sdf = ET.Element("sdf", {"version": "1.12"})
model = ET.SubElement(sdf, "model", {"name": "sample_robot"})
pose(model, relative_to="world")
base = ET.SubElement(model, "link", {"name": "base_link"})
visual = ET.SubElement(base, "visual", {"name": "base_visual"})
geometry = ET.SubElement(visual, "geometry")
box = ET.SubElement(geometry, "box")
text(box, "size", "0.4 0.3 0.1")
return {
"xml": sdf,
"assumptions": ["base_link is coincident with the model frame."],
"metadata": {"target_consumer": "visualization"},
}
```