first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
@@ -0,0 +1,136 @@
# SDF builder helpers
Builder helpers are optional. They exist to reduce common LLM mistakes in XML construction, not to replace SDFormat or libsdformat.
## Why helpers exist
LLMs often make these mistakes when writing raw XML:
- wrong pose value length;
- hidden degree/radian conversion;
- missing `relative_to` or `expressed_in`;
- zero or non-finite joint axes;
- negative primitive dimensions;
- mesh scale applied inconsistently;
- inertial values copied from visuals;
- plugin filenames invented from plausible names.
Helpers should make the common path explicit, typed-ish, and auditable while still returning ordinary `xml.etree.ElementTree.Element` nodes.
## Design constraints
- Standard library only.
- No mandatory Gazebo, ROS, NumPy, lxml, CAD, or mesh dependency.
- Return ElementTree elements.
- Compose with raw ElementTree calls.
- Validate local numeric shape and finiteness.
- Do not attempt full SDFormat schema validation.
- Do not silently invent frames, axes, inertials, plugins, or sensor parameters.
## Recommended helper surface
Names may vary to match the current runtime package, but the helper surface should stay small.
```python
# XML basics
text(parent, tag, value, attrib=None)
fmt_float(value)
fmt_vector(values)
# Poses and axes
pose(parent, xyz=(0, 0, 0), rpy=(0, 0, 0), *, relative_to=None,
rotation_format="euler_rpy", degrees=False)
quat_pose(parent, xyz=(0, 0, 0), quat_xyzw=(0, 0, 0, 1), *, relative_to=None)
axis(parent, xyz=(0, 0, 1), *, expressed_in=None)
# Document structure
sdf_root(version="1.12")
world(parent, name)
model(parent, name, *, static=None, pose=None)
frame(parent, name, *, attached_to=None, pose=None)
link(parent, name, *, pose=None, inertial=None)
joint(parent, name, joint_type, parent_link, child_link, *, pose=None,
axis_xyz=None, axis_expressed_in=None, axis2_xyz=None, limits=None)
# Geometry
visual(parent, name, *, pose=None)
collision(parent, name, *, pose=None)
box(parent, size_xyz)
sphere(parent, radius)
cylinder(parent, radius, length)
capsule(parent, radius, length)
mesh(parent, uri, *, scale=None)
# Physics and metadata
inertial(parent, mass, inertia, *, pose=None)
sensor(parent, name, sensor_type, *, pose=None, topic=None, update_rate=None)
plugin(parent, name, filename, params=None)
include(parent, uri, *, name=None, pose=None)
```
## Numeric behavior
Helpers should reject:
- non-finite numbers;
- vectors with the wrong length;
- negative or zero primitive dimensions;
- zero joint axes;
- zero-mass inertials;
- zero-norm quaternions.
Helpers may warn, but should not silently fix:
- non-unit axes;
- non-normalized quaternions;
- use of degrees;
- missing `relative_to` on nontrivial poses.
## Example
```python
from sdf.builder import axis, box, collision, joint, link, model, sdf_root, visual
BASE_SIZE_M = (0.4, 0.3, 0.1)
LIFT_AXIS_Z = (0.0, 0.0, 1.0)
def gen_sdf():
sdf = sdf_root("1.12")
robot = model(sdf, "lift_fixture", static=False)
base = link(robot, "base_link")
v = visual(base, "base_visual")
box(v, BASE_SIZE_M)
c = collision(base, "base_collision")
box(c, BASE_SIZE_M)
carriage = link(robot, "carriage_link")
j = joint(
robot,
"lift_joint",
"prismatic",
parent_link="base_link",
child_link="carriage_link",
axis_xyz=LIFT_AXIS_Z,
axis_expressed_in="base_link",
)
return {
"xml": sdf,
"assumptions": [
{"code": "inertials_placeholder", "message": "Inertials omitted pending measured masses."}
],
}
```
## When not to use helpers
Raw ElementTree is acceptable when:
- a target simulator needs unusual extension XML;
- a plugin has arbitrary nested configuration;
- an existing generator already has a clear internal abstraction;
- the user asked for a minimal XML-only source.
Even then, keep named constants, ledger comments, and validation.
@@ -0,0 +1,115 @@
# SDF design ledger
Create or update this ledger before writing SDF XML. The ledger can live in the response, a project note, or comments in the generator source. The goal is to externalize spatial and simulator assumptions before they become hard-to-audit XML.
## Document
| Field | Value |
|---|---|
| Output path | |
| Source file | |
| SDF version | `1.12` unless constrained |
| Document kind | model / world / model-in-world |
| Target consumer | Gazebo / other simulator / visualization-only / model package |
| Units | meters, kilograms, seconds, radians unless documented otherwise |
| Coordinate convention | REP-103-like / simulator-specific / documented exception |
| World support needed | yes/no; reason |
| Optional external checks | `gz sdf --check`, simulator load, CAD Viewer, other |
## Model or world scope
| Item | Value |
|---|---|
| Model/world name | |
| Static or dynamic | |
| Canonical link, if relevant | |
| Model/world pose | xyz + rpy/quaternion |
| Model/world pose `relative_to` | |
| Includes | URI + purpose |
| World physics/lights/plugins | source and target simulator |
## Frames
| Frame | Scope | Attached to | Pose | Pose `relative_to` | Purpose | Source |
|---|---|---|---|---|---|---|
| | | | | | | |
Use named frames for clarity when multiple sensors, nested models, tool frames, plugin frames, or repeated transforms depend on the same relationship.
## Links
| Link | Physical / frame-like | Pose | Pose `relative_to` | Inertial source | Sensor/plugin attached | Notes |
|---|---|---|---|---|---|---|
| | | | | | | |
Physical dynamic links need inertials. Frame-like links may omit inertials only when documented.
## Joints
| Joint | Type | Parent | Child | Pose | Pose frame / `relative_to` | Axis | Axis frame / `expressed_in` | Limits | Positive motion | Source |
|---|---|---|---|---|---|---|---|---|---|---|
| | | | | | | | | | | |
For revolute and prismatic joints, record limit units: radians for revolute, meters for prismatic. Continuous joints should not be given artificial finite position limits unless a simulator-specific reason is documented.
## Geometry
| Owner | Visual/collision | Name | Geometry type | Pose | Pose `relative_to` | URI or dimensions | Mesh units | Scale | Source |
|---|---|---|---|---|---|---|---|---|---|
| | | | | | | | | | |
Collision geometry should be selected for simulation cost and stability, not just visual similarity.
## Inertials
| Link | Mass | COM pose | Inertia tensor | Method/source | Confidence |
|---|---|---|---|---|---|
| | | | | | |
Mark approximations clearly. Very small, zero, negative, or guessed inertias are high risk for simulation.
## Sensors and plugins
| Element | Parent | Pose/frame | Filename/type | Parameters | Source docs | Assumptions |
|---|---|---|---|---|---|---|
| | | | | | | |
Do not invent plugin filenames, topics, frame names, namespaces, controller parameters, or update rates. Derive them from simulator documentation or user-provided configuration.
## Mesh URI policy
| URI kind | Allowed? | Resolution expectation | Notes |
|---|---|---|---|
| Relative local path | | Relative to generated `.sdf` location | |
| `file://` | | Absolute local file | |
| `model://` | | Simulator model path | |
| `package://` | | Simulator/ROS package environment | |
| `fuel://`, `http://`, `https://` | | External resource | |
## Assumptions to report
List every guessed or inferred value:
- transform or pose;
- axis sign or positive-motion convention;
- mesh unit or scale;
- mass, COM, or inertia;
- target simulator behavior;
- plugin parameter;
- unresolved external URI;
- skipped validation or smoke test.
If a value cannot be derived or safely assumed, generate a minimal placeholder only when the user asked for a placeholder, and label it as such.
## Compact response template
```text
SDF source: path/to/source.py
Generated target: path/to/output.sdf
Target consumer: Gazebo Harmonic, SDF 1.12
Bundled validation: passed with 2 warnings
External checks: gz sdf --check skipped, gz not installed
Assumptions:
- Assumed mesh units are meters.
- Assumed camera optical frame follows simulator plugin documentation.
```
@@ -0,0 +1,148 @@
# SDF examples
These examples illustrate the intended style: explicit frames, source-relative assumptions, simple helpers, and structured assumptions.
## Minimal model with raw ElementTree
```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 gen_sdf():
sdf = ET.Element("sdf", {"version": "1.12"})
model = ET.SubElement(sdf, "model", {"name": "calibration_box"})
text(model, "pose", "0 0 0 0 0 0", {"relative_to": "world"})
link = ET.SubElement(model, "link", {"name": "body"})
visual = ET.SubElement(link, "visual", {"name": "body_visual"})
geometry = ET.SubElement(visual, "geometry")
box = ET.SubElement(geometry, "box")
text(box, "size", "0.1 0.1 0.1")
collision = ET.SubElement(link, "collision", {"name": "body_collision"})
c_geometry = ET.SubElement(collision, "geometry")
c_box = ET.SubElement(c_geometry, "box")
text(c_box, "size", "0.1 0.1 0.1")
inertial = ET.SubElement(link, "inertial")
text(inertial, "mass", "1.0")
inertia = ET.SubElement(inertial, "inertia")
text(inertia, "ixx", "0.0016666667")
text(inertia, "iyy", "0.0016666667")
text(inertia, "izz", "0.0016666667")
text(inertia, "ixy", "0")
text(inertia, "ixz", "0")
text(inertia, "iyz", "0")
return {
"xml": sdf,
"assumptions": ["Box inertia uses a uniform-density primitive approximation."],
}
```
## Minimal world
```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 gen_sdf():
sdf = ET.Element("sdf", {"version": "1.12"})
world = ET.SubElement(sdf, "world", {"name": "empty_lit_world"})
light = ET.SubElement(world, "light", {"name": "sun", "type": "directional"})
text(light, "pose", "0 0 10 0 0 0", {"relative_to": "world"})
text(light, "cast_shadows", "true")
return {
"xml": sdf,
"metadata": {"document_kind": "world"},
"assumptions": ["World intentionally contains no inline model."],
}
```
## Model with builder helpers
```python
from sdf.builder import (
box,
collision,
inertial,
joint,
link,
model,
pose,
sdf_root,
visual,
)
BASE_MASS_KG = 2.0
ARM_MASS_KG = 0.5
def gen_sdf():
sdf = sdf_root("1.12")
robot = model(sdf, "two_link_demo")
pose(robot, relative_to="world")
base = link(robot, "base_link")
box(visual(base, "base_visual"), (0.4, 0.3, 0.1))
box(collision(base, "base_collision"), (0.4, 0.3, 0.1))
inertial(base, BASE_MASS_KG, (0.02, 0.03, 0.04, 0, 0, 0))
arm = link(robot, "arm_link")
box(visual(arm, "arm_visual"), (0.3, 0.05, 0.05))
box(collision(arm, "arm_collision"), (0.3, 0.05, 0.05))
inertial(arm, ARM_MASS_KG, (0.001, 0.004, 0.004, 0, 0, 0))
joint(
robot,
"shoulder_pan",
"revolute",
"base_link",
"arm_link",
axis_xyz=(0, 0, 1),
axis_expressed_in="base_link",
)
return {
"xml": sdf,
"assumptions": [
"Primitive inertials are approximate placeholders.",
"Positive shoulder_pan rotates counterclockwise when viewed from +Z."
],
}
```
## Plugin block from documentation
When a plugin block is copied from target simulator documentation, preserve its explicit parameters and report the source:
```python
import xml.etree.ElementTree as ET
def add_known_plugin(parent):
plugin = ET.SubElement(
parent,
"plugin",
{"name": "example_control", "filename": "libexample_control.so"},
)
ET.SubElement(plugin, "namespace").text = "robot1"
return plugin
```
Do not invent plugin fields. If the documentation source is not available, mark the plugin as unverified in the generator envelope.
@@ -0,0 +1,87 @@
# SDF frame and pose semantics
Use this reference before editing any SDF `<pose>`, `<frame>`, `<joint>`, `<axis>`, `<visual>`, `<collision>`, sensor, or plugin placement.
## Core pose rules
A typical SDF pose is:
```xml
<pose relative_to="some_frame">x y z roll pitch yaw</pose>
```
or, when using quaternion rotation:
```xml
<pose rotation_format="quat_xyzw" relative_to="some_frame">x y z qx qy qz qw</pose>
```
Rules to keep in mind:
- The first three values are position.
- With the default `rotation_format="euler_rpy"`, the pose has six values: `x y z roll pitch yaw`.
- With `rotation_format="quat_xyzw"`, the pose has seven values: `x y z qx qy qz qw`.
- Euler angles are radians by default. `degrees="true"` is valid SDF but should be avoided in generated sources unless the target explicitly requires it.
- `relative_to` names the frame in which the pose is expressed.
- If `relative_to` is omitted, SDF applies element-specific defaults, commonly the frame of the parent XML element. This may be valid but is easy to misread. Prefer explicit `relative_to` for nontrivial generated poses.
- Nested scopes may use `::`, for example `outer_model::inner_model::sensor_frame`.
## Joint pose and axes
For SDF joints:
- `<parent>` names the parent frame or `world`.
- `<child>` names the child frame; `world` is not valid as the child.
- Joint pose defaults are easy to misinterpret. Use explicit `<pose relative_to="...">` when the joint frame is not obviously the child-link frame.
- `<axis><xyz>...</xyz></axis>` is the unit axis vector.
- An axis is expressed in the joint frame unless the axis `expressed_in` attribute specifies another frame.
- `axis2` is used for multi-axis joints such as `revolute2` and `universal`.
- Axis vectors should be finite, nonzero, and normalized.
Record the expected positive motion in the design ledger. Example: “positive shoulder_pan rotates the arm counterclockwise when viewed from +Z.”
## Visual and collision poses
A `<visual>` or `<collision>` pose places that geometry owner relative to its parent frame unless `relative_to` says otherwise. In ordinary model-level use, that parent is the link frame.
Do not use visual offsets to hide a wrong link or joint frame. If a mesh needs an offset because the mesh asset origin is not the link frame, record that fact in the geometry table.
## Named frames
Use `<frame>` when a reusable transform is meaningful:
```xml
<frame name="camera_optical_frame" attached_to="camera_link">
<pose relative_to="camera_link">0 0 0 -1.57079632679 0 -1.57079632679</pose>
</frame>
```
Frames are useful for sensors, plugins, tool frames, nested models, and repeated placement logic. They also make generated SDF more auditable.
`attached_to` and `relative_to` are different:
- `attached_to` says what the frame moves with.
- `relative_to` says how the frame's pose numbers are represented.
The `attached_to` chain should not cycle and should eventually resolve to a link, model, world, joint, or another valid frame target.
## LLM guardrails
Do not infer any of the following from prose alone:
- sign of a joint axis;
- frame in which an axis is expressed;
- RPY order or units;
- mesh origin convention;
- `relative_to` frame;
- nested-scope reference;
- sensor optical-frame transform;
- plugin frame/topic semantics.
When data is missing, either ask for the source data or write an explicitly labeled assumption.
## Useful official references
- SDFormat pose semantics: `https://sdformat.org/tutorials?tut=pose_frame_semantics`
- SDFormat pose fields: `https://sdformat.org/spec/1.12/world/`
- SDFormat joint element: `https://sdformat.org/spec/1.12/joint/`
@@ -0,0 +1,55 @@
# SDF generation command
Use the SDF launcher to regenerate explicit SDFormat outputs from Python sources with top-level `gen_sdf()` functions.
```bash
python scripts/sdf path/to/model.py
python scripts/sdf path/to/model.py -o path/to/robot.sdf
python scripts/sdf path/to/a.py=out/a.sdf path/to/b.py=out/b.sdf
```
Plain Python targets write sibling `.sdf` files beside their sources. `-o` / `--output` is valid only with one plain target. Use `SOURCE.py=OUTPUT.sdf` pairs for custom multi-target destinations.
Relative source targets and CLI output paths resolve from the current working directory. When running from outside the skill directory, prefix the launcher path so source and target files still resolve from the intended workspace.
## What the command does
The tool should:
1. import the target Python source;
2. call top-level zero-argument `gen_sdf()`;
3. normalize the returned SDF XML or envelope;
4. parse the generated XML;
5. run bundled dependency-light validation;
6. optionally run external `gz sdf --check` if requested and available;
7. write the requested `.sdf` only after required checks pass;
8. print structured warnings and assumptions.
The command does **not** regenerate geometry, meshes, GLB/topology outputs, render assets, robot-description files, planning metadata, or simulator resource packages. Regenerate those with their owning workflows before regenerating SDF that references them.
## Optional external check
If implemented, the recommended flag is:
```bash
python scripts/sdf path/to/model.py --gz-check auto
python scripts/sdf path/to/model.py --gz-check required
python scripts/sdf path/to/model.py --gz-check never
python scripts/sdf path/to/model.py --strict
```
- `auto`: run `gz sdf --check` when `gz` is available; otherwise report the check as skipped and continue.
- `required`: fail if `gz` is unavailable or if `gz sdf --check` exits nonzero.
- `never`: skip external checking.
If the external checker requires a path, write to a temporary file first. Do not overwrite the target until bundled validation and all required external checks pass.
## Failure behavior
If validation fails, the newly generated payload is not written. Existing output files may be stale; fix the Python source and regenerate.
Errors should be blocking. Warnings and assumptions should be reported but should not fail generation unless the user or CI explicitly requests strict behavior.
## Execution safety
The launcher imports generator modules. Top-level Python code in generator files may execute. Use this command only for trusted project sources. A future subprocess runner may reduce accidental side effects, but it cannot make untrusted Python safe.
@@ -0,0 +1,151 @@
# 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"},
}
```
@@ -0,0 +1,56 @@
# SDF implementation notes
These notes describe the intended runtime shape after the rewrite. Keep this file accurate when implementation changes.
## Implemented behavior
The SDF skill should:
- generate explicit targets only;
- validate generated XML before writing;
- preserve existing `gen_sdf()` return compatibility: XML element, XML string, or envelope dict with `xml`;
- accept envelope `assumptions`, `warnings`, and `metadata` fields;
- support `--strict` warning-as-failure behavior;
- optionally run `gz sdf --check` through `--gz-check auto|required|never`;
- provide optional stdlib-only authoring helpers;
- parse SDF XML from files or in-memory strings;
- resolve local mesh files relative to the generated output location;
- accept external mesh URI schemes without local filesystem resolution;
- allow pure world files when structurally valid.
## Intended bundled validation scope
The bundled validator should check common structural and numeric errors:
- root element and version;
- document shape and pure world support;
- required names and duplicate names in local scopes;
- pose value counts, rotation formats, finite values, degrees usage, quaternion normalization, and local `relative_to` resolution;
- named frame attachment references and cycles;
- joint type set, parent/child references, axis/axis2 values, limits, and dynamics numbers;
- visual/collision owner names and geometry presence;
- primitive geometry dimensions and mesh URI/scale;
- local mesh path existence;
- inertial mass and inertia tensor plausibility;
- sensor name/type/update-rate structure;
- plugin name/filename structure.
## Remaining limitations
The bundled validator is still not a full libsdformat or simulator validator. It should not claim to fully validate:
- every version-specific SDFormat schema rule;
- all nested-model frame semantics;
- transform math or resolved poses;
- mesh unit conventions;
- arbitrary mesh inertia or collision quality;
- simulator-specific physics settings;
- plugin schemas and runtime availability;
- sensor runtime behavior;
- target simulator support for every element.
Use the design ledger, structured diagnostics, `gz sdf --check`, simulator load tests, and explicit reporting of skipped checks.
## Execution safety
The current launcher imports generator modules in-process. Generator Python files execute code and must be trusted project sources.
@@ -0,0 +1,76 @@
# SDF interoperability notes
Use this reference when SDF work touches upstream geometry, robot-description data, Gazebo/libsdformat, model packages, or CAD Viewer.
## Geometry assets
SDF should reference geometry and mesh assets; it should not regenerate them.
When SDF references generated meshes, record:
- source geometry file;
- exported mesh path;
- mesh unit convention;
- mesh origin convention;
- visual scale;
- collision simplification decision.
Regenerate geometry and mesh artifacts with their owning workflow before regenerating SDF if geometry changed.
## Robot descriptions
Keep the simulator document aligned with the upstream robot-description source when one exists.
Upstream robot-description data usually owns:
- link and joint structure used by robot-state publishing;
- physical joint limits;
- inertials and visual/collision geometry when that source is authoritative;
- control-related structure and runtime interfaces.
Use SDF for simulator/world concerns:
- simulator plugins;
- sensors requiring simulator-specific XML;
- surfaces/contact/friction;
- lights, terrain, physics, and worlds;
- nested models and includes;
- simulator-specific metadata.
Do not use SDF to paper over a wrong upstream frame tree unless the task explicitly targets a simulator-only model.
## Planning metadata
SDF should not define planning groups, end-effectors, group states, or disabled-collision matrices. If the task becomes IK or path-planning work, use the planning metadata workflow that owns those semantics.
## CAD Viewer
CAD Viewer can review `.sdf` files visually through `$cad-viewer` and help catch gross placement or resource issues. It cannot prove simulator dynamics, inertial validity, plugin loading, sensor topics, or joint-axis semantics.
Pass explicit generated or modified `.sdf` paths to `$cad-viewer` whenever it is available, and return the live viewer link it prints.
CAD Viewer renders SDF as static structure plus direct inspection controls. It lists plugins, sensors, lights, includes, and nested models as metadata, but does not execute plugins or consume file-authored motion contracts.
## Gazebo / libsdformat
The bundled validator is a lightweight preflight check. Use the target simulator's parser and loader when compatibility matters.
Good checks include:
```bash
gz sdf --check path/to/model.sdf
```
and a real simulator load in the target environment.
## Model packages and URIs
SDF resource resolution is environment-dependent. Record which URI forms the target consumer can resolve:
- relative paths from the generated `.sdf` location;
- `model://` paths under the simulator model path;
- `package://` paths under ROS/package resolution;
- `fuel://` resources;
- `http://` or `https://` assets if external fetches are allowed.
The bundled validator can confirm local relative paths, but it cannot prove external simulator resource paths unless the target environment is available.
@@ -0,0 +1,115 @@
# LLM guardrails for SDF authoring
This skill assumes agents are useful at structuring SDFormat documents and weak at silently deriving precise spatial, physical, and simulator-specific values. The workflow should route those weaknesses into explicit ledgers, constants, helpers, validators, and smoke tests.
## What agents can usually do well
- organize an SDF model or world into links, joints, frames, visuals, collisions, sensors, plugins, and includes;
- translate user intent into a plausible document structure;
- maintain naming consistency when names are explicit;
- write small Python generators and ElementTree transformations;
- explain assumptions and create checklists;
- preserve existing patterns when examples are nearby.
## What agents should not be trusted to infer silently
- exact link poses, frame transforms, or joint origins;
- positive joint-axis directions from visual appearance;
- mesh units, mesh scale, or coordinate-system conventions;
- center of mass or inertia tensors from rendered shape alone;
- plugin filenames, parameters, topics, namespaces, or sensor schemas;
- whether a plugin is a simulator runtime plugin or a CAD Viewer visualization-only extension;
- target simulator support for a given SDFormat version or extension;
- whether collision geometry is stable for physics;
- whether external URIs resolve in the deployment environment.
## Required mitigation pattern
For every spatial, physical, or simulator-specific value, use one of these sources:
1. user-provided requirement;
2. upstream geometry, robot-description, planning-metadata, mesh manifest, or model package source;
3. target simulator documentation;
4. measured or calculated value with method stated;
5. explicit assumption recorded in the generator envelope, design ledger, or final report.
Do not hide guessed values in raw XML.
## Placeholder policy
Placeholders are allowed only when the user asks for a scaffold, draft, or minimal example. Mark them as placeholders and keep them easy to replace.
Examples of acceptable placeholders:
```python
return {
"xml": sdf,
"assumptions": [
{
"code": "placeholder_inertial",
"message": "Inertial tensor uses a primitive approximation pending measured mass properties.",
}
],
}
```
Examples of unacceptable placeholders:
- invented plugin filenames;
- adding CAD Viewer-only motion plugins to SDF files;
- arbitrary inertia values on a dynamic robot without a warning;
- guessed mesh scale that makes the visual look plausible;
- silently flipping a joint axis to match an expected screenshot.
## Spatial reasoning checklist
Before generating or modifying SDF, answer these questions in the ledger or final report:
| Question | Required evidence |
|---|---|
| What frame is each pose expressed in? | `relative_to`, source file, or documented default |
| What frame is each joint axis expressed in? | `expressed_in` or documented default |
| What is positive motion for each non-fixed joint? | command/test expectation or upstream source |
| Are mesh units and scales known? | manifest, CAD export config, or explicit assumption |
| Are visual and collision poses intentionally different? | simulation reason or source geometry |
| Are inertials measured, calculated, approximated, or omitted? | method and confidence |
| Are plugin and sensor parameters copied from target docs? | target simulator/version and source |
## Code-generation style
Prefer this pattern:
```python
BASE_TO_CAMERA_XYZ_M = (0.18, 0.0, 0.12)
BASE_TO_CAMERA_RPY_RAD = (0.0, -0.2, 0.0)
CAMERA_FRAME = "camera_frame"
# Source: project CAD frame export 2026-05-12. RPY radians.
pose(camera_frame, BASE_TO_CAMERA_XYZ_M, BASE_TO_CAMERA_RPY_RAD, relative_to="base_link")
```
Avoid this pattern:
```python
ET.SubElement(camera, "pose").text = "0.18 0 .12 0 -11.5 0"
```
The second version hides units, uses degrees without saying so, and makes the source of the transform impossible to audit.
## Validation expectations
The validator should catch cheap deterministic mistakes, but it cannot prove the design is physically or simulator-correct. After bundled validation, use optional external checks and simulator smoke tests when the task depends on simulator behavior.
Report skipped checks explicitly. A skipped check is not automatically a failure, but it is relevant risk information.
## Response behavior for agents
When finishing an SDF task, state:
- source generator path and generated target path;
- checks run and their result;
- checks skipped and why;
- assumptions and placeholders;
- risks that need simulator verification.
Do not simply say that the file is valid. Say which validator or smoke test passed.
@@ -0,0 +1,79 @@
# SDF workflow
Use this reference when editing SDF robot model structure, world structure, mesh references, simulator metadata, or generated SDF output.
## Edit loop
1. Find the Python source that defines `gen_sdf()`.
2. Treat that source as authoritative. Do not hand-edit generated `.sdf` output unless explicitly instructed.
3. Identify the target consumer and required SDFormat version.
4. Decide whether the output is model-level, world-level, or model-in-world.
5. Fill or update the design ledger before writing XML.
6. For every pose and axis, state the frame in which it is expressed. Use `relative_to` / `expressed_in` where ambiguity would otherwise remain.
7. Edit the generator source.
8. Regenerate only the explicit target.
9. Review bundled validation errors as structural guardrails, not exhaustive simulator proof.
10. Hand generated or modified `.sdf` files to `$cad-viewer` for live viewer links when available.
11. Run available smoke tests.
12. Report assumptions and skipped checks.
## Model vs world
Use **model-level SDF** when exporting a reusable robot or object model that another world can include.
Use **world-level SDF** when the task includes:
- physics engine settings;
- lights or scene setup;
- terrain or ground plane;
- multiple initial model placements;
- world plugins;
- includes of external model packages;
- simulator scene setup.
Use **model-in-world SDF** when the task explicitly needs both an inline model and world-specific context.
The lightweight validator should allow pure world-only documents. A world-only document with lights, physics, actors, or includes can be valid SDFormat even when it contains no inline `<model>`.
## Mesh references
SDF mesh URIs should be stable from the generated `.sdf` file's perspective or use a simulator/package URI convention understood by the consumer.
Good URI choices include:
- relative paths beside the generated SDF when the model is self-contained;
- `model://...` for simulator model packages;
- `package://...` when the simulator environment resolves package roots;
- `fuel://...`, `http://...`, or `https://...` only when the consumer is expected to fetch external assets.
Do not use generated SDF XML as the source of truth for mesh placement. Prefer deriving visual and collision mesh references from the same source data that owns mesh instance placement.
## Inertials and physics
For dynamic models, inertial data is simulation-critical. If inertials are estimated, record the approximation method. Do not copy visual origins into inertial origins unless that is physically justified.
Collision geometry should be selected for stable and fast physics, not visual fidelity. Use primitive or simplified collision geometry when possible.
## Plugins and sensors
For plugins and sensors, record:
- plugin filename or sensor type;
- expected simulator distribution/version;
- topics, frames, update rates, namespaces;
- parameter source;
- startup smoke test result.
Do not invent plugin parameters. Incorrect plugin XML can pass lightweight validation and still fail at simulator load time.
CAD Viewer reviews SDF files as static model/world structure through `$cad-viewer` links. Do not add Explorer-only motion plugins; use simulator-native controllers, plugins, or test harnesses for simulator behavior.
## Existing SDF inspection
When inspecting existing `.sdf` files, separate three questions:
1. Is the XML structurally valid enough for the bundled validator?
2. Is it compatible with the target SDFormat/libsdformat/simulator version?
3. Does it satisfy this project's packaging, mesh, and workflow policy?
Do not reject valid SDF solely because it violates a project preference unless the task or repository policy requires that preference.
@@ -0,0 +1,98 @@
# SDF smoke tests
Use smoke tests after generated SDF passes bundled validation. The goal is to catch simulator and spatial failures that dependency-light XML checks cannot detect.
## Recommended checks
### Bundled validation
```bash
python scripts/sdf path/to/model.py
python scripts/sdf path/to/model.py --strict
```
Bundled validation runs during explicit target generation. Use `--strict` when warnings should block handoff.
### SDFormat parser check
When Gazebo tooling is installed:
```bash
gz sdf --check path/to/model.sdf
```
or through the skill CLI:
```bash
python scripts/sdf path/to/model.py --gz-check auto
```
Use the exact simulator environment that will consume the file when possible.
### Simulator load check
Load the model or world in the target simulator and check:
- no parser warnings or plugin load errors;
- model appears at the intended pose;
- visual and collision assets resolve;
- collision geometry is not visibly offset from visuals;
- dynamic model does not explode, fall through the floor, or produce invalid inertia warnings.
### Joint motion check
For each non-fixed joint:
- command a small positive motion;
- confirm the moving child moves in the expected direction;
- confirm limits stop motion where expected;
- confirm continuous joints can rotate continuously if intended.
### CAD Viewer static review
After generating or modifying an `.sdf`, hand the explicit path to `$cad-viewer` for a live viewer link when available.
- confirm direct model links, joints, frames, visuals, and collisions are placed correctly;
- confirm includes, plugins, sensors, lights, nested models, and unsupported geometry are listed as static metadata;
- record any simulator-only behavior that CAD Viewer cannot execute.
### Sensor and plugin check
For each sensor or plugin:
- confirm plugin library loads;
- confirm expected topics/services appear;
- confirm frame names match the design ledger;
- confirm update rate and namespace behavior;
- capture one sample output if practical.
### Visual review
When CAD Viewer or an equivalent viewer is available through `$cad-viewer`, return the viewer link. Visual review is useful but insufficient: it can catch gross placement and mesh problems, but it cannot prove axis frames, inertials, dynamics, or plugin behavior.
## Report format
Use a compact report:
```text
Checks run:
- bundled SDF validation: passed
- gz sdf --check: skipped, gz not installed
- simulator load: passed in Gazebo Harmonic
- joint motion: shoulder_pan positive motion verified; gripper joints skipped
- plugin startup: camera plugin unresolved, requires target simulator package
Assumptions:
- Assumed mesh units are meters.
- Assumed lidar frame is coincident with lidar_link.
```
## When to stop
Stop and fix the generator when:
- bundled validation has errors;
- `gz sdf --check` fails under a required external-check policy;
- the simulator reports invalid inertias or unresolved required assets;
- a joint moves opposite from the documented positive direction;
- plugin startup fails for a plugin required by the task.
@@ -0,0 +1,154 @@
# SDF validation
Generation validates every `gen_sdf()` result before writing. This validation is dependency-light and intended to catch common structural errors. It is not a replacement for libsdformat, Gazebo, or target-simulator validation.
## Validation model
The validator should produce structured diagnostics with severities:
- `error`: invalid or unsafe enough to block writing output;
- `warning`: likely problem or unverified simulator behavior; output can be written unless `--strict` is used;
- `info`: assumption, skipped check, or useful context.
`--strict` treats warnings as failures.
## Bundled checks
### Root and document shape
The runtime should check that:
- the root element is `<sdf>`;
- the root has a non-empty `version` attribute;
- the version looks like `major.minor`;
- the document contains meaningful SDF content such as a model, world, actor, light, include, or plugin;
- structurally valid pure world files are accepted even when they contain no inline model.
### Names and scopes
The runtime should check that:
- world names are non-empty and unique at root scope;
- root model names are non-empty and unique;
- model link, joint, frame, sensor, visual, and collision names are non-empty where required and unique within their owner scope;
- duplicate names are reported with a path and scope.
### Poses
The runtime should check all `<pose>` elements:
- default `rotation_format="euler_rpy"` has exactly six finite values;
- `rotation_format="quat_xyzw"` has exactly seven finite values;
- unsupported `rotation_format` is an error;
- quaternion values are approximately normalized;
- `degrees="true"` is a warning unless strict mode is enabled;
- nontrivial omitted `relative_to` is a warning;
- `relative_to` resolves within local scope when possible;
- nested `::` references have valid syntax and resolve when the local tree is available.
### Frames
The runtime should check that:
- `<frame name="...">` has a non-empty unique name in its scope;
- `attached_to`, when present, resolves locally when possible;
- frame attachment chains do not cycle;
- unresolved nested or external frame references are reported as warnings when local validation cannot prove them invalid.
### Joints
Known SDF 1.12 joint types:
```text
continuous, revolute, gearbox, revolute2, prismatic, ball, screw, universal, fixed
```
The runtime should check that:
- joint type is non-empty and known;
- `<parent>` and `<child>` text exists;
- `world` is allowed as parent but not child;
- unscoped parent/child references exist in the same model;
- `axis` and `axis2` vectors are finite, nonzero, and normalized;
- `axis2` is used only where the joint type supports a second axis;
- `expressed_in` resolves when local resolution is possible;
- limit and dynamics values are finite or documented infinities where SDFormat permits them;
- finite lower limits do not exceed finite upper limits;
- continuous joints with fake finite position limits produce a warning.
### Geometry and mesh URIs
The runtime should check that:
- each visual/collision owner has one geometry element;
- each geometry has exactly one known primitive or mesh child when possible;
- box size has 3 positive finite values;
- cylinder radius and length are positive and finite;
- sphere radius is positive and finite;
- plane size has 2 positive finite values;
- mesh URI values are non-empty;
- mesh scale has 3 positive finite values when present;
- local mesh references resolve relative to the generated `.sdf` location;
- known external URI schemes such as `model://`, `package://`, `fuel://`, `http://`, and `https://` are accepted without local filesystem resolution.
### Inertials
The runtime should check that:
- mass is positive and finite;
- inertial pose is valid when present;
- inertia tensor components are finite;
- inertia matrix is positive semidefinite within tolerance;
- missing inertial data on dynamic physical links is at least a warning;
- frame-like or static links can omit inertials when documented.
### Sensors and plugins
The runtime should check that:
- sensor names are non-empty and unique within owner scope;
- sensor `type` is non-empty;
- sensor `update_rate`, when present, is finite and non-negative;
- sensor pose is valid;
- plugin filename is non-empty;
- plugin name, when present, is non-empty;
- arbitrary simulator-specific plugin schemas are not invented by the validator.
Plugin filenames and parameters can pass bundled validation and still fail in the target simulator. Use smoke tests.
### CAD Viewer review
CAD Viewer treats SDF plugins, sensors, lights, includes, and nested models as static metadata. The bundled validator checks generic structure only; it does not validate Explorer-only motion contracts or execute simulator plugins.
After generated `.sdf` files are created or modified, hand explicit paths to `$cad-viewer` for live viewer links when available.
This plugin is for CAD Viewer visualization and review. It is not a Gazebo physics/controller plugin and should not be represented as simulator runtime behavior.
## External checks
When Gazebo tooling is available, run:
```bash
gz sdf --check path/to/file.sdf
```
The CLI option should be:
```bash
python scripts/sdf path/to/source.py --gz-check auto
```
External checks should be recorded in the diagnostics report. A skipped optional check is not a bundled-validation failure unless the user requested `--gz-check required`.
## SDF validity vs project policy
Separate these categories:
| Category | Examples |
|---|---|
| SDF structural validity | root `<sdf>`, version, legal element shape, non-empty names, references |
| Numeric plausibility | finite poses, positive dimensions, positive mass, normalized axes, PSD inertia |
| Simulator compatibility | libsdformat version, supported joint types, plugin availability, sensor support |
| Project policy | mesh location, preferred URI style, STL/DAE preference, collision simplification, no unresolved external URIs |
Do not reject valid SDF merely because it violates a project policy unless the task or repository requires that policy. Prefer warnings and strict-mode controls.