vcad uses an Intermediate Representation (IR) to bridge Rust and TypeScript implementations. The IR is a JSON-serializable format that describes CAD operations as a directed acyclic graph (DAG).
Why an IR?
The IR serves several purposes:
- Language interop — Same format works in Rust (
vcad-ir) and TypeScript (@vcad/ir) - Serialization — Save and load models as
.vcadJSON files - Tooling — Build viewers, editors, and analysis tools
- AI agents — Structured format that LLMs can generate directly
Document Structure
A vcad document has four main sections:
interface Document {
version: "0.1";
nodes: Record<string, Node>;
materials: Record<string, MaterialDef>;
roots: SceneEntry[];
}
Nodes
Nodes form a DAG where each node represents an operation:
interface Node {
id: number;
name: string | null;
op: CsgOp;
}
Nodes reference other nodes by ID, creating the dependency graph.
Operations (CsgOp)
Operations are tagged unions. Each type has specific fields:
// Primitives
{ type: "Cube", size: { x, y, z } }
{ type: "Cylinder", radius, height, segments }
{ type: "Sphere", radius, segments }
{ type: "Cone", radiusBottom, radiusTop, height, segments }
// Booleans
{ type: "Union", left: nodeId, right: nodeId }
{ type: "Difference", left: nodeId, right: nodeId }
{ type: "Intersection", left: nodeId, right: nodeId }
// Transforms
{ type: "Translate", child: nodeId, offset: { x, y, z } }
{ type: "Rotate", child: nodeId, angles: { x, y, z } }
{ type: "Scale", child: nodeId, factor: { x, y, z } }
// Patterns
{ type: "LinearPattern", child: nodeId, direction: vec3, count, spacing }
{ type: "CircularPattern", child: nodeId, axis, count, angle }
Materials
PBR material definitions:
interface MaterialDef {
name: string;
color: [number, number, number]; // RGB, 0-1
metallic: number; // 0-1
roughness: number; // 0-1
density?: number; // kg/m³ (optional)
description?: string;
}
Scene Roots
The roots array defines which nodes to render and their materials:
interface SceneEntry {
root: number; // Node ID
material: string; // Material key
}
Example Document
Here's a complete document for a plate with holes:
{
"version": "0.1",
"nodes": {
"1": {
"id": 1,
"name": "plate",
"op": { "type": "Cube", "size": { "x": 100, "y": 60, "z": 5 } }
},
"2": {
"id": 2,
"name": null,
"op": { "type": "Translate", "child": 1, "offset": { "x": -50, "y": -30, "z": -2.5 } }
},
"3": {
"id": 3,
"name": "hole",
"op": { "type": "Cylinder", "radius": 3, "height": 10, "segments": 32 }
},
"4": {
"id": 4,
"name": null,
"op": { "type": "Translate", "child": 3, "offset": { "x": 40, "y": 20, "z": 0 } }
},
"5": {
"id": 5,
"name": "result",
"op": { "type": "Difference", "left": 2, "right": 4 }
}
},
"materials": {
"aluminum": {
"name": "Aluminum",
"color": [0.9, 0.9, 0.92],
"metallic": 0.95,
"roughness": 0.3,
"density": 2700
}
},
"roots": [
{ "root": 5, "material": "aluminum" }
]
}
Compact IR Format
For machine learning applications, vcad provides a Compact IR format — a token-efficient text representation designed for model training and inference.
Why Compact IR?
- Token efficiency — ~5x fewer tokens than JSON for same models
- Line-based — Each line is a node, line number is the node ID
- Unambiguous — Simple grammar, easy to parse and generate
- ML-friendly — Designed for LLM training on CAD generation
Format Specification
# Primitives (line number = node ID)
C sx sy sz # Cube
Y r h # Cylinder
S r # Sphere
K rb rt h # Cone (bottom radius, top radius, height)
# Booleans
U a b # Union
D a b # Difference
I a b # Intersection
# Transforms
T n dx dy dz # Translate
R n rx ry rz # Rotate (degrees)
X n sx sy sz # Scale
# Patterns
LP n dx dy dz count spacing # Linear pattern
CP n ox oy oz ax ay az count angle # Circular pattern
# Modifier
SH n thickness # Shell
Example
The same plate with hole in Compact IR:
C 100 60 5
T 0 -50 -30 -2.5
Y 3 10
T 2 40 20 0
D 1 3
Compare to the 40-line JSON version above — same geometry in 5 lines.
Usage
TypeScript:
import { fromCompact, toCompact } from "@vcad/ir";
const doc = fromCompact("C 50 30 5\nY 5 10\nT 1 25 15 0\nD 0 2");
const compact = toCompact(doc);
Rust:
use vcad_ir::compact::{from_compact, to_compact};
let doc = from_compact("C 50 30 5\nY 5 10\nT 1 25 15 0\nD 0 2")?;
let compact = to_compact(&doc)?;
WASM:
import { parseCompactIR, evaluateCompactIR } from "@vcad/kernel-wasm";
const json = parseCompactIR("C 50 30 5");
const solid = evaluateCompactIR("C 50 30 5\nY 5 10\nT 1 25 15 0\nD 0 2");
Sketch Blocks
Sketches use a block syntax with SK header and END terminator:
SK 0 0 0 1 0 0 0 1 0 # origin, x_dir, y_dir
L 0 0 10 0 # Line segment
L 10 0 10 5
A 10 5 5 10 7.5 7.5 1 # Arc (start, end, center, ccw)
L 5 10 0 5
L 0 5 0 0
END
E 0 0 0 20 # Extrude sketch 0 by (0,0,20)
DAG Evaluation
The IR is evaluated bottom-up:
- Start from root nodes
- Recursively evaluate dependencies
- Cache intermediate results (nodes may be referenced multiple times)
function evaluate(doc: Document, nodeId: number): Mesh {
const node = doc.nodes[nodeId];
switch (node.op.type) {
case "Cube":
return createCubeMesh(node.op.size);
case "Difference":
const left = evaluate(doc, node.op.left);
const right = evaluate(doc, node.op.right);
return booleanDifference(left, right);
// ... etc
}
}
The @vcad/engine package provides a complete evaluator that uses the manifold-3d WASM module for geometry operations.
TypeScript Types
The @vcad/ir package exports all IR types:
import type {
Document,
Node,
CsgOp,
MaterialDef,
SceneEntry,
Vec3,
} from "@vcad/ir";
Rust Types
The vcad-ir crate provides equivalent types with serde serialization:
use vcad_ir::{Document, Node, CsgOp, MaterialDef};
// Serialize to JSON
let json = serde_json::to_string_pretty(&document)?;
// Deserialize from JSON
let doc: Document = serde_json::from_str(&json)?;
Design Decisions
Why a DAG?
A DAG (vs a tree) allows:
- Reuse — A hole pattern can be referenced by multiple difference operations
- Efficiency — Shared subexpressions are computed once
- History — The graph preserves construction history
Why JSON?
- Human-readable for debugging
- Universal language support
- Easy to generate from LLMs
- Simple tooling (jq, editors, etc.)
Why not a tree structure?
Trees require duplicating shared geometry. The DAG with explicit node references is more memory-efficient and preserves the logical structure.
File Extension
vcad documents use the .vcad extension:
model.vcad → JSON document
The format is intentionally simple JSON with no compression. For large models, standard compression (gzip) works well.