# How to Create SDF Simulator Models with Physics Using text-to-cad

> Easily create SDF simulator models with physics using text-to-cad. This tool automates inertial data, joint constraints, and collision geometry for robust simulation exports.

- Repository: [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad)
- Tags: how-to-guide
- Published: 2026-07-22

---

**The text-to-cad repository ships a dedicated SDF skill that provides builder and validation APIs for programmatically authoring physics-enabled SDF files, ensuring inertial data, joint constraints, and collision geometry meet simulation standards before export.**

The earthtojake/text-to-cad repository includes a specialized SDF skill for generating Simulation Description Format (SDF) files that define physics-enabled robot models. Creating reliable SDF simulator models with physics requires more than XML markup—it demands valid inertial properties, proper joint kinematics, and collision geometry that passes static analysis. This guide walks through the builder API in [`skills/sdf/scripts/sdf/builder.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/builder.py) and the validation pipeline in [`skills/sdf/scripts/sdf/validation.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/validation.py) to produce simulation-ready assets.

## Constructing the SDF Structure with the Builder API

The builder API provides thin wrappers around XML generation to enforce SDF schema compliance while constructing hierarchical simulation elements.

### Initializing the Root and World

Use `sdf_root(version)` to create the top-level `<sdf>` element, defaulting to version 1.12. The `world()` helper establishes the simulation environment container.

```python
from skills.sdf.scripts.sdf.builder import sdf_root, world, model, link, inertial
import xml.etree.ElementTree as ET

# Create SDF 1.12 root and a world container

root = sdf_root(version="1.12")
w = world(root, "example_world")

```

### Adding Models and Links

The `model()` helper creates dynamic or static model containers, while `link()` defines rigid bodies requiring inertial properties for physics simulation.

```python

# Create a dynamic model (static=False requires inertial data)

m = model(w, "my_robot", static=False, pose=[0, 0, 0, 0, 0, 0])

# Add a base link with mass properties

base_link = link(m, "base_link")
inertial(base_link, mass=5.0, inertia=[0.1, 0.1, 0.1, 0, 0, 0])

```

### Defining Joints and Motion Constraints

Use `joint()` to connect links with specific kinematic types (revolute, prismatic, etc.) and configure axis vectors and limit constraints.

```python
from skills.sdf.scripts.sdf.builder import joint

# Connect base_link to arm_link with a revolute joint

joint(m, "shoulder_joint", "revolute", 
      parent_link="base_link", 
      child_link="arm_link",
      axis_xyz=[0, 0, 1], 
      limits={"lower": -1.57, "upper": 1.57, "effort": 50, "velocity": 1.0})

```

## Specifying Physical Properties for Dynamics

Physics simulation requires precise mass distribution, collision boundaries, and motion constraints defined through specific XML blocks.

### Inertial Data and Mass Properties

In [`skills/sdf/scripts/sdf/builder.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/builder.py), the `inertial()` helper constructs the `<inertial>` block requiring mass and a six-component inertia matrix `[ixx, iyy, izz, ixy, ixz, iyz]`. The validator in [`skills/sdf/scripts/sdf/validation.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/validation.py) checks that this matrix is positive semidefinite via `_inertia_is_psd()`.

Dynamic links must have mass greater than zero and a physically realistic inertia tensor. The builder enforces basic type checking, while the validator ensures the matrix satisfies mathematical constraints for rigid-body dynamics.

### Collision Geometry and Visual Meshes

Collision geometry should differ from visual meshes for simulation efficiency. The `collision()` helper accepts a geometry builder function, while the validator warns against `collision_reuses_visual_mesh` to prevent performance issues in the simulator.

```python
from skills.sdf.scripts.sdf.builder import visual, collision, mesh, box

# High-detail visual mesh

visual(base_link, "base_visual", geometry_builder=lambda g: mesh(g, "meshes/base.stl"))

# Simplified collision geometry (box instead of mesh)

collision(base_link, "base_collision", geometry_builder=lambda g: box(g, [0.5, 0.5, 0.2]))

```

### Joint Axes and Limit Validation

Joint axes must be non-zero vectors (preferably unit length) defined via `axis_xyz`. The validator's `_validate_axis()` function checks for `non_unit_axis` warnings and validates that continuous joints don't have finite limits, while revolute and prismatic joints require consistent lower/upper bounds.

## Validating Physics Correctness Before Export

The validation pipeline in [`skills/sdf/scripts/sdf/validation.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/validation.py) catches semantic errors that schema validation misses, ensuring the SDF simulator models with physics will load correctly in Gazebo or other simulators.

### Static Analysis Checks

`validate_sdf_root()` performs comprehensive static analysis including:

- **Duplicate names detection** (`duplicate_names`) across worlds, models, links, and joints
- **Frame cycle detection** via depth-first search (`_validate_frame_cycles`) to prevent infinite recursion in frame attachments
- **Mesh URI resolution** (`_validate_mesh_uri`) verifying that referenced STL or DAE files exist on disk
- **Scoped reference validation** (`invalid_scoped_reference`) ensuring parent/child link names in joints resolve correctly

### Handling Validation Errors

Validation results are stored in `ValidationResult` objects defined in [`skills/sdf/scripts/sdf/findings.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/findings.py). The `raise_for_validation_errors()` function converts findings into human-readable `SdfSourceError` exceptions via [`source.py`](https://github.com/earthtojake/text-to-cad/blob/main/source.py), halting execution if physics constraints are violated.

```python
from pathlib import Path
from skills.sdf.scripts.sdf.validation import validate_sdf_root, raise_for_validation_errors

# Serialize and validate

sdf_xml = ET.tostring(root, encoding="utf-8").decode()
result = validate_sdf_root(ET.fromstring(sdf_xml), source_path=Path("model.sdf"))

# Raise SdfSourceError if validation fails

raise_for_validation_errors(result, strict=True)

```

## Complete Workflow Example

This end-to-end example demonstrates creating a two-link robot with physics, collision geometry, and proper validation:

```python
import xml.etree.ElementTree as ET
from pathlib import Path
from skills.sdf.scripts.sdf.builder import (
    sdf_root, world, model, link, joint,
    inertial, visual, collision, mesh, box
)
from skills.sdf.scripts.sdf.validation import validate_sdf_root, raise_for_validation_errors

# 1️⃣ Create the XML skeleton

root = sdf_root(version="1.12")
w = world(root, "physics_world")

# 2️⃣ Add a dynamic model

m = model(w, "articulated_arm", static=False, pose=[0, 0, 0.5, 0, 0, 0])

# 3️⃣ Define base link with inertial and visual geometry

base = link(m, "base_link")
inertial(base, mass=5.0, inertia=[0.1, 0.1, 0.1, 0, 0, 0])
visual(base, "base_visual", geometry_builder=lambda g: mesh(g, "meshes/base.stl"))
collision(base, "base_collision", geometry_builder=lambda g: box(g, [0.5, 0.5, 0.2]))

# 4️⃣ Add second link with inertial properties

arm = link(m, "arm_link")
inertial(arm, mass=1.0, inertia=[0.02, 0.02, 0.02, 0, 0, 0])

# 5️⃣ Connect with revolute joint

joint(m, "shoulder", "revolute", 
      parent_link="base_link", 
      child_link="arm_link",
      axis_xyz=[0, 0, 1], 
      limits={"lower": -1.57, "upper": 1.57, "effort": 50, "velocity": 1.0})

# 6️⃣ Serialize to string

sdf_string = ET.tostring(root, encoding="utf-8").decode()

# 7️⃣ Validate before saving

result = validate_sdf_root(ET.fromstring(sdf_string), source_path=Path("arm.sdf"))
raise_for_validation_errors(result, strict=True)

# Write to file if validation passes

with open("arm.sdf", "w") as f:
    f.write(sdf_string)

```

## Summary

- The **builder API** in [`skills/sdf/scripts/sdf/builder.py`](https://github.com/earthtojake/text-to-cad/blob/main/skills/sdf/scripts/sdf/builder.py) provides type-safe XML generation for SDF elements including worlds, models, links, and joints.
- **Inertial data** must include positive mass and a positive semidefinite inertia matrix checked by `_inertia_is_psd()` in the validator.
- **Collision geometry** should be simplified compared to visual meshes to avoid `collision_reuses_visual_mesh` warnings.
- **Joint axes** require normalization (unit vectors) and appropriate limit constraints for the joint type to pass `_validate_axis()`.
- **Static validation** via `validate_sdf_root()` catches duplicate names, frame cycles, and missing mesh files before the model reaches the simulator.

## Frequently Asked Questions

### What SDF version does text-to-cad support?

The builder defaults to SDF version 1.12 via `sdf_root(version="1.12")`, though the XML structure accepts other version strings as specified in the SDFormat specification.

### How does the validator check if an inertia matrix is physically valid?

The validation pipeline calls `_inertia_is_psd()` to verify that the 3x3 inertia matrix constructed from the six components `[ixx, iyy, izz, ixy, ixz, iyz]` is positive semidefinite, ensuring the mass distribution represents a physically possible rigid body.

### Can I use the same mesh file for both visual and collision geometry?

While technically possible, the validator issues a `collision_reuses_visual_mesh` warning because high-polygon visual meshes degrade physics simulation performance. Use simplified collision geometries (boxes, cylinders, or low-poly meshes) instead.

### What happens if my SDF contains invalid joint axis definitions?

The `_validate_axis()` function checks for zero-length vectors and non-unit axes. Non-unit axes trigger `non_unit_axis` warnings but pass validation, while missing axis definitions for non-fixed joints generate validation errors that raise `SdfSourceError` when `raise_for_validation_errors()` is called.