C
CADVERT
Developers
About
Open App →
cadvert · python library

Build AI tools that
understand CAD files

cadvert is a Python library that reads STEP, IGES, BREP, STL, and OBJ files and converts them into structured text context your LLM can reason about — geometry, features, tolerances, dimensions, and rendered views, all in one call.

pip install cadvert

Installation

The base package includes CAD parsing and full geometry analysis. Install optional extras for additional capabilities:

bash
# Core — CAD parsing, topology, features, spatial analysis
pip install cadvert

# + REST API server (FastAPI + Uvicorn)
pip install cadvert[server]

# + LLM integration (OpenAI)
pip install cadvert[llm]

# + Mesh support (OBJ files via trimesh)
pip install cadvert[mesh]

# Everything
pip install cadvert[full]

Requires Python 3.10+. The core package ships cadquery-ocp (OCCT bindings) and numpy — no other heavy dependencies.

Quick start

One call takes a CAD file to LLM-ready context — and every other representation:

python
import cadvert

result = cadvert.analyze("bracket.step")

print(result.to_text())     # LLM-ready structured document (the flagship)
result.to_dict()            # JSON-safe analysis for quoting / CI / databases
result.to_json()
result.to_graph()           # networkx face-adjacency graph  (GNN / UV-Net)
result.to_points(2048)      # (N,3) surface point cloud       (PointNet / 3D-CNN)

analyze() runs the full pipeline (ingest → topology → features → spatial) and returns a CadvertResult that exposes every representation from one object. to_text() is a compact document (~5–15 KB) with the part's global properties, geometry, detected features, and spatial relationships — everything an LLM needs to reason about the file.

Prefer the pipeline? The five stages remain importable individually (cadvert.ingest, cadvert.topology, cadvert.features, cadvert.spatial, cadvert.document) — see the API reference below. analyze() just wires them together for the common case.

analyze() also handles .dxf 2D drawings, returning a DxfResult with the same to_text() / to_dict() interface — entities, layers, dimensions, and title-block text.

How it works

Under the hood, analyze() runs five stages — load_step → build_topology → detect_features → compute_spatial → render_tier0 — each returning plain Python dataclasses, with no OCC objects leaking past the ingest boundary. Every stage is importable on its own (see the API reference below).

What's in the output document?

SectionWhat's included
Global propertiesBounding box, volume, surface area, center of mass, body count
Topology mapAll faces and edges with surface types (plane, cylinder, sphere, NURBS…)
Detected featuresHoles (diameter/depth), bosses, fillets, chamfers, slots, patterns
Spatial relationshipsWall thicknesses, feature distances, draft angles, undercuts
GD&T annotationsTolerances from AP242 STEP files with PMI (if present)
Tip All geometry is analytical — extracted directly from OCC B-REP, not tessellated. Values match what was authored in the originating CAD software exactly.

Supported formats

ExtensionFormatPipelineFeatures & Spatial
.step / .stp STEP ISO 10303 (AP203, AP214, AP242) Full B-REP Yes — including GD&T for AP242
.iges / .igs IGES 5.x Full B-REP Yes
.brep OpenCASCADE native Full B-REP Yes
.stl STereoLithography Mesh only Global properties only
.obj Wavefront OBJ Mesh only Global properties only (requires cadvert[mesh])
Note Mesh formats (STL, OBJ) don't carry analytical surface definitions, so topology analysis, feature detection, and spatial relationships are not available for them. The output will still include triangle count, bounding box, and surface area.

API reference

cadvert.ingest

Entry point for loading any supported CAD format.

python
from cadvert.ingest import load_step, IngestError, PartMetadata

shape, body_count, metadata = load_step(path: str | Path)
# shape       → OCC TopoDS_Shape (pass to build_topology / render_shape)
# body_count  → int, number of distinct solid bodies (0 for mesh files)
# metadata    → PartMetadata dataclass

# PartMetadata fields:
metadata.source_format      # "STEP" | "IGES" | "BREP" | "STL" | "OBJ"
metadata.is_mesh            # True for STL/OBJ — triggers mesh-only pipeline
metadata.units              # "mm" | "inch" | "m" | ...
metadata.unit_scale_to_mm   # float multiplier to convert to mm
metadata.schema             # "AP203" | "AP214" | "AP242" (STEP only)
metadata.originating_system # CAD software that authored the file
metadata.description        # FILE_DESCRIPTION from STEP header
metadata.triangle_count     # populated for mesh formats
metadata.gdt_annotations    # list[GDTAnnotation] (AP242 PMI only)

# Raises IngestError on unsupported format or parse failure

cadvert.topology

Extracts the full face/edge/vertex graph from a loaded B-REP shape.

python
from cadvert.topology import build_topology, TopologyGraph

graph = build_topology(shape, body_count)  # → TopologyGraph

graph.faces           # list[FaceInfo]  — surfaces with geometry + connectivity
graph.edges           # list[EdgeInfo]  — curves with dihedral angles + convexity
graph.vertices        # list[VertexInfo]
graph.bounding_box    # {"X": (min, max), "Y": (...), "Z": (...)}
graph.volume          # float, native units
graph.surface_area    # float, native units
graph.center_of_mass  # (x, y, z)
graph.body_count      # int

# FaceInfo.geometry dict — surface type determines fields:
# PLANE:    {"type": "PLANE",    "normal": (nx,ny,nz), "origin": (x,y,z), "d": offset}
# CYLINDER: {"type": "CYLINDER", "axis_origin": ..., "axis_dir": ..., "radius": r}
# CONE:     {"type": "CONE",     "apex": ..., "axis_dir": ..., "half_angle": deg}
# SPHERE:   {"type": "SPHERE",   "center": ..., "radius": r}
# TORUS:    {"type": "TORUS",    "center": ..., "major_radius": R, "minor_radius": r}
# NURBS_SURFACE: {"type": "NURBS_SURFACE", "degree_u": ..., "degree_v": ..., ...}

# EdgeInfo.convexity: "convex" | "concave" | "tangent"

cadvert.features

Detects manufacturing features from the topology graph.

python
from cadvert.features import detect_features, DetectedFeature

features = detect_features(graph)  # → list[DetectedFeature]

# DetectedFeature fields:
feature.feature_type  # "THROUGH_HOLE" | "BLIND_HOLE" | "BOSS" | "FILLET" |
                      # "CHAMFER" | "POCKET" | "COUNTERBORE" | "COUNTERSINK" |
                      # "SLOT" | "PATTERN"
feature.face_ids      # list[int] — which faces comprise this feature
feature.edge_ids      # list[int]
feature.parameters    # dict — feature-specific (e.g. diameter, depth, axis for holes)
feature.confidence    # float 0.0–1.0 (features below 0.4 are suppressed)
feature.standard_match # str | None — e.g. "M6 clearance", "1/4\" thru"
feature.notes         # human-readable description

cadvert.spatial

Computes distances, wall thicknesses, draft angles, and undercut detection.

python
from cadvert.spatial import compute_spatial_relationships

spatial = compute_spatial_relationships(
    graph,
    features,
    shape=shape,                  # optional — enables advanced ray-based analysis
    pull_direction=(0, 0, 1),     # for draft angle / undercut analysis
)  # → list[SpatialRelationship]

# SpatialRelationship fields:
rel.description  # "Wall thickness" | "Draft angle" | "Distance" | ...
rel.value        # float — mm (or degrees for angles)
rel.from_ref     # e.g. "hole_1", "F42"
rel.to_ref       # e.g. "outer_surface", "F10"
rel.notes        # additional context

cadvert.document

Renders the analysis results into structured text for LLM consumption.

python
from cadvert.document import render_tier0, render_document

# Compact summary — best for LLM system prompts (~5–15 KB)
text = render_tier0(
    graph,
    source_path,
    features=features,           # optional
    spatial=spatial,             # optional
    units=metadata.units,
    gdt_annotations=metadata.gdt_annotations,  # optional
)

# Full multi-section HSD document — complete topology dump
full_text = render_document(
    graph,
    source_path,
    part_name=None,              # override display name
    features=features,
    spatial=spatial,
    rendered_views=views,        # list of PNG paths from render_shape()
    validation_report=None,
    units=metadata.units,
    gdt_annotations=metadata.gdt_annotations,
    mesh_info=None,
)

cadvert.renderer

Renders 8 standard orthographic + isometric views as PNG files.

python
from cadvert.renderer import render_shape, render_views

# From a pre-loaded OCC shape
views = render_shape(
    shape,
    output_dir="./views",
    image_size=(1200, 900),
    stem="part",
)  # → list[Path] — front, back, left, right, top, bottom, iso, ...

# Convenience: load + render in one call
views = render_views(
    step_path="bracket.step",
    output_dir="./views",
    image_size=(1200, 900),
)

Building an LLM tool

The recommended pattern is to wrap cadvert's pipeline into a single function that you register as a tool in your LLM framework. The tool receives a file path, runs the full pipeline, and returns the structured text context.

python — generic tool wrapper
from cadvert.ingest import load_step, IngestError
from cadvert.topology import build_topology
from cadvert.features import detect_features
from cadvert.spatial import compute_spatial_relationships
from cadvert.document import render_tier0
from cadvert.renderer import render_shape

def analyze_cad(file_path: str) -> dict:
    """
    Load a CAD file and return structured context for an LLM.
    Supports .step, .stp, .iges, .igs, .brep, .stl, .obj
    """
    try:
        shape, body_count, metadata = load_step(file_path)
    except IngestError as e:
        return {"error": str(e)}

    result = {
        "format": metadata.source_format,
        "units": metadata.units,
        "is_mesh": metadata.is_mesh,
    }

    if not metadata.is_mesh:
        graph = build_topology(shape, body_count)
        features = detect_features(graph)
        spatial = compute_spatial_relationships(graph, features, shape=shape)

        result["context"] = render_tier0(
            graph, file_path,
            features=features,
            spatial=spatial,
            units=metadata.units,
            gdt_annotations=metadata.gdt_annotations,
        )
    else:
        # Mesh files: global properties only
        result["context"] = f"Mesh file: {metadata.triangle_count} triangles."

    # Optional: render views and return image paths
    # result["views"] = [str(p) for p in render_shape(shape, "./views")]

    return result

Example: Claude tool use

Register analyze_cad as a tool in an Anthropic API call. Claude will invoke it when the user references a CAD file, then reason over the structured output.

Your API key Get a key from console.anthropic.com, then set it once in your terminal before running your script:

export ANTHROPIC_API_KEY="sk-ant-..."

The anthropic.Anthropic() client reads it automatically. You can also pass it directly: anthropic.Anthropic(api_key="sk-ant-...")
python
import anthropic
import json

client = anthropic.Anthropic()

tools = [
    {
        "name": "analyze_cad",
        "description": (
            "Read a CAD file (.step, .iges, .brep, .stl, .obj) and return "
            "structured geometry, detected features (holes, bosses, fillets, …), "
            "spatial relationships, dimensions, and tolerances."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "Absolute path to the CAD file on disk"
                }
            },
            "required": ["file_path"]
        }
    }
]

def run_agent(user_message: str):
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )

        # Append assistant turn
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use" and block.name == "analyze_cad":
                    result = analyze_cad(block.input["file_path"])
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result),
                    })

            messages.append({"role": "user", "content": tool_results})

        else:
            # Final text response
            for block in response.content:
                if hasattr(block, "text"):
                    print(block.text)
            break

run_agent("Analyze bracket.step and tell me if it's suitable for CNC machining.")
Tip — vision models Pass the PNG views from render_shape() alongside the text context to a vision-capable model (Claude, GPT-4o) for even richer analysis. The text covers numbers; the images cover spatial intuition.

CLI reference

cadvert also ships a command-line tool for one-off conversions:

bash
# Convert any supported file to an HSD document
cadvert part.step -o output.hsd.txt

# Skip specific pipeline stages
cadvert part.step --no-features --no-spatial --no-render --no-validate

# Include full NURBS control-point data
cadvert part.step --nurbs-detail

# Set draft analysis direction
cadvert part.step --pull-direction 0,0,-1

# Override render resolution
cadvert part.step --image-size 1600x1200

# Start the REST API server (requires cadvert[server])
cadvert-server
# Listens on http://localhost:8000
Large parts Files with more than 100 faces automatically split output into part.hsd.txt (summary) and part.full_topology.txt (complete face/edge dump). Pass the summary to your LLM system prompt; fetch the full topology on demand.