# Cad Khana

> Diagnostics-first CAD wrapper around Build123d: assembly-level interference/clearance assertions plus optional per-part printability checks. Load BEFORE editing an `assembly.py` that uses the wrapper or interpreting its diagnostic JSON — SKILL.md has conventions the scripts rely on but don't restate. TRIGGER: about to run `khana check`/`build`/`view`/`draw`, or editing a file that imports `cad_kh…

- **Type:** Skill
- **Install:** `agentstack add skill-cyberchitta-cad-khana-cad-khana`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cyberchitta](https://agentstack.voostack.com/s/cyberchitta)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [cyberchitta](https://github.com/cyberchitta)
- **Source:** https://github.com/cyberchitta/cad-khana/tree/main/skills/cad-khana

## Install

```sh
agentstack add skill-cyberchitta-cad-khana-cad-khana
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# cad-khana

cad-khana splits geometric reasoning into two workflows:

- **Mechanism** — relational checks on an assembly (no interference,
  clearance between parts). Expressed via `Assembly.assert_*(...)` and
  evaluated by `check(assembly, out=...)`. Writes `mechanism.json`.
- **Printability** — per-part, per-manufacturing-method checks (min
  wall thickness, overhangs). Expressed via `inspect(part, method=...)`.
  Writes `-printability.json`.

A script typically does both: composes an `Assembly`, calls `check()`,
then calls `inspect()` once per printed part.

## Setup

If `khana --version` fails, follow `references/install.md` once before
proceeding.

## When to use this tool

- Designing a **multi-part mechanical assembly** that needs to fit
  together (hinges, snap-fits, sliders, clevis/pin joints, boxes with
  lids).
- Producing **printable geometry** where wall thickness, clearance, and
  overhangs matter.
- Iterating under **agent control** — the JSON diagnostics are the
  primary signal; `khana draw` supplements it with engineering-drawing
  PNGs (HLR line-art) you can read directly when shape-level questions
  come up.

## When not to use it

- Pure surface modeling, organic shapes, meshes from scans. Use
  Build123d directly or a mesh tool.
- CAM / toolpath generation. Out of scope.
- Full constraint solving (drive geometry from relationships). The tool
  uses *assertions* — they check, they don't drive.

## CLI

```
khana build            # run script, export STL/STEP, write JSON diagnostics
khana check            # run script, write JSON diagnostics only (no export)
khana view             # build, then push assembly to the OCP viewer (socket)
khana draw    [--view ] [--part ] [--format png|svg|both] [--themeable]  # build, then write engineering drawings under /views/
khana diff      # diff two JSON files (mechanism or printability)
khana status                   # JSON probe of versions + viewer reachability; exit nonzero if degraded
khana --version
```

Prefer `khana check` during fast iteration — it skips STL/STEP export
so the loop is tighter. Switch to `khana build` when you want the
exports on disk.

JSON diagnostics are always written to `--out` (default `outputs/`),
even on failure — read them to diagnose errors. Exit code is nonzero
on any assertion failure or script exception.

**Output location.** A relative `out=` passed to `check()` / `inspect()`
inside a script is anchored to the script file's directory, so
`out="outputs"` lands next to the script regardless of the cwd you
invoked `khana` from. Same for `khana`'s default `--out` (used for
error diagnostics if the script crashes before reaching `check()`).
Pass an absolute path, or an explicit `--out ` (cwd-relative,
because you typed it), to override.

### Viewer: no editor required

`khana view` calls `ocp_vscode.show(...)`, which pushes geometry over
a local socket (default port 3939). The listener can be either the
**OCP CAD Viewer** VS Code extension *or* the **standalone viewer
server** that ships with `ocp_vscode`:

```
uv run python -m ocp_vscode          # opens a browser tab, listens on 3939
uv run khana view assembly.py        # pushes geometry to whichever listener is up
```

So you can drive the full `view` loop from any editor (or none at
all). For **Zed**, the pattern that matches the VS Code UX is a pair
of workspace tasks in `.zed/tasks.json` — one to start the viewer
server, one to push the current file to it:

```json
[
  {
    "label": "OCP viewer: start",
    "command": "uv",
    "args": ["run", "python", "-m", "ocp_vscode"],
    "cwd": "$ZED_WORKTREE_ROOT",
    "allow_concurrent_runs": false
  },
  {
    "label": "khana view (current file)",
    "command": "cd \"$ZED_DIRNAME\" && uv run khana view \"$ZED_FILE\""
  }
]
```

## Script structure

Keep four sections, in order:

1. **Parameters + derived** — named constants at the top, so one change
   propagates through everything.
2. **Pure part functions** — each returns a `Part`. Take parameters with
   defaults; no hidden globals, no mutation.
3. **Assembly composition** — build an `Assembly` by chaining `.with_part()`
   and `.assert_*()` calls. Call `check(assembly, out="outputs")`.
4. **Per-part printability** — one `inspect(part, method=FDM(), name=...)`
   call per printed part.

See `references/examples/pin_hinge/assembly.py` for the canonical
example.

## Designing a new mechanism

When starting from a blank file, do these steps **in this order**.
Out-of-order work — most often, drawing before scalars are clean —
burns cycles on geometry that the diagnostics would have rejected for
free.

1. **Declare parts as pure functions.** One function per distinct
   printed body, taking parameters with defaults. No globals, no
   placement inside the function.
2. **Wire the assembly with explicit `Location`s.** Compose with
   `Assembly().with_part(name, part(), location=…)`. Names are stable IDs
   the assertions and diagnostics reference.
3. **Add `assert_no_interference` between *every* candidate-overlap
   pair immediately** — before any clearance work. The cost of
   asserting a pair that will never collide is one line; the cost of
   *not* asserting a pair that silently overlaps is a printed part
   you can't assemble. Default to over-asserting.
4. **Add `assert_clearance(a, b, min_mm=…)` between every pair of
   parts that move relative to each other.** Pick a real number
   (≥ 0.2 mm for FDM at 0.4 mm nozzle) — not a placeholder you mean
   to revisit.
5. **Run `khana check` and iterate until all scalars are green.**
   Reading `mechanism.json` is the primary loop; do not draw yet.
6. **Then** `khana draw` for shape-level verification. See
   **Reading drawings** for which view answers which kind of question.

The first pass at a new mechanism is the moment to be liberal with
assertions; pruning later (because one is provably redundant) is
cheap, but discovering a missing one downstream is expensive.

## Minimal skeleton

```python
from build123d import Box, Cylinder, Location, Part, Pos, Rot

from cad_khana.mechanism.assembly import Assembly
from cad_khana.mechanism.check import check
from cad_khana.printability.inspect import inspect
from cad_khana.printability.methods import FDM

# 1. parameters + derived
WIDTH = 40.0
HEIGHT = 20.0
PIN_D = 3.0
PIN_CLEARANCE = 0.25
PIN_HOLE_D = PIN_D + 2 * PIN_CLEARANCE

# 2. pure part functions
def bracket(w: float = WIDTH, h: float = HEIGHT) -> Part:
    return Pos(0, 0, h / 2) * Box(w, w, h)

def pin(length: float = WIDTH, d: float = PIN_D) -> Part:
    return Cylinder(d / 2, length)

# 3. assembly + mechanism assertions
assembly = (
    Assembly()
    .with_part("bracket", bracket())
    .with_part("pin", pin(), location=Location((0, 0, HEIGHT / 2)) * Rot(90, 0, 0))
    .assert_no_interference("pin", "bracket")
)

if __name__ == "__main__":
    check(assembly, out="outputs")
    # 4. printability checks for each printed part
    inspect(bracket(), method=FDM(), out="outputs", name="bracket")
```

## Recommended style

These conventions make a script **re-editable** — the next session can
bump a parameter and the design updates consistently.

For build123d's selector operators (`>`, `>`, `"` string that
  downstream consumers (chitra-cad's photo-real renderer; future FEA /
  kinematics) resolve against their own catalogs. Same intrinsic-vs-
  placement rule as color: set it at the `.with_part()` site when the same
  part body gets placed with different materials (or when the parent
  is the natural place to bind it); push it inside the part-builder
  only if the part has one intrinsic material everywhere it's used;
  leave unset (`None`) when the answer is genuinely open and let the
  consumer's override layer supply the current best guess. For
  cross-consumer experiments (render + FEA both reading from the same
  assembly), use `Assembly.with_materials({name: token})`. For
  render-only sweeps, use the consumer's own override (e.g.
  chitra-cad's `Scene.with_materials({...})`).
- **Two fidelity tiers — keep cheap geometry in the assembly,
  apply detail as an override layer.** The geometric-iteration
  loop (interference, clearance, printability) runs on cheap
  primitives — `Box(20, 20, L)` for a 2020 extrusion, no
  fasteners. That's the right model for assertions: it's fast to
  tessellate, and a real V-slot profile is a strict subset of a
  solid 20×20 so any clearance the cheap model passes the detailed
  one passes too. Detailed geometry (real `bd_warehouse` profiles,
  fasteners, finished shapes) lives in a `/detail_variations.py`
  module as named bundles and applies via
  `Assembly.with_detailed_geometry(BUNDLE)` before the consumer
  (render / FEA / kinematics) reads the assembly. The override map
  handles **both swaps and additions**: a key matching an existing
  `PlacedPart.name` swaps the part shape (placement / material /
  color preserved); a key with no match appends a new `PlacedPart`
  from a `DetailOverride(part=…, location=…, material=…)`.
  Fasteners that the cheap model never created enter via additions
  — and each new fastener earns its own clearance assertion at the
  sub-assembly that owns the joint. Same intrinsic-vs-placement
  rule as materials: stable detail facts can move into the
  part-builder when they earn it; live as override entries until
  then. The two override layers (`with_materials`,
  `with_detailed_geometry`) compose — call them in either order
  before handing the assembly to the consumer.
- **Algebraic mode operators (`+`, `-`, `*`, `Pos`, `Rot`) read more
  cleanly than `BuildPart` for short shapes** — prefer them unless the
  BuildPart context buys something (sketches, workplanes, patterns).
- **Name parts with stable identifiers** when you place them — assertions
  reference these names, and the JSON diagnostics report per-name data.
- **Inspect only the parts you will actually print.** Stand-ins
  (extrusion stubs, shafts, fixed hardware) don't need `inspect()`;
  they are bought, not printed.
- **Document the coordinate frame in the module docstring** whenever
  the axes carry non-trivial meaning (radial vs tangential, hinge
  axis, floor datum, etc.). Without this, the next reader has to
  reverse-engineer axis conventions from the part math, and will
  often guess wrong. A 3-to-5-line block is enough:

  ```text
  Coordinate frame:
      origin = column axis ∩ floor datum
      +X     = radial outward toward the exit opening
      +Y     = tangent at the opening (hinge axis)
      +Z     = up
      z=0    = bearing/spider base
  ```

## Parametric standard parts: bd_warehouse

`bd_warehouse` is a Build123d-native companion library bundled as a
default dependency — fasteners, bearings, modeled threads, gears,
sprockets, pipes, flanges, and OpenBuilds extrusions. Reach for it
before hand-rolling any standard hardware. Each class subclasses
`BasePartObject`, so an instance *is* a `Part`. Wrap it in a thin pure
part function to keep script style consistent:

```python
from bd_warehouse.fastener import HexNut

def lock_nut(size: str = "M8-1.25") -> Part:
    return HexNut(size=size, fastener_type="iso4032")
```

Don't `inspect()` parts that come from `bd_warehouse` — they're bought,
not printed.

For what's in the library and how to discover available classes,
parameters, and valid type/size strings, load
`references/standard_parts.md`.

## Available mechanism assertions

Every assertion records a result in `mechanism.json`. If any fail,
`check()` raises `SystemExit(1)`. All failures are collected — you get
every problem in one pass, not just the first.

| Assertion | Checks |
|---|---|
| `.assert_no_interference(a, b)` | Parts `a` and `b` don't overlap (intersection volume ≤ 0.001 mm³). |
| `.assert_clearance(a, b, min_mm=…)` | Minimum distance between `a` and `b` is at least `min_mm`. |
| `.assert_interference(a, b, reason=…)` | Parts `a` and `b` **do** overlap (intersection volume > 0.001 mm³). Regression alarm for a documented, accepted overlap — fails if the overlap disappears, forcing the assertion to be removed when the design gap gets fixed. |

Give assertions a `name=` when you'd benefit from a specific label in
the diagnostics; otherwise they get an auto-generated one.

`assert_interference` is the exception, not the rule. Use it only when
a real design constraint leaves an overlap that hasn't been resolved
yet (e.g., a junction whose bracket hasn't been designed). The
`reason=` string is included in the failure message when the overlap
goes away, so a future reader understands what the assertion was
guarding against. Default to `assert_no_interference` everywhere else.

## Animation: joints + time-parameterized assembly

Beyond static assertions, `Assembly` can express **motion**: a
`RevoluteJoint` on a `with_subassembly(...)` exposes a single
animatable DOF, and a `t → Assembly` factory function (the project's
animation primitive) drives the joints from a time parameter.

Use this when:

- A mechanism's clearance / interference depends on a joint angle —
  not just the rest pose. Sample `factory(t)` at several `t` and
  call `check()` on each to catch mid-motion overlaps.
- You're producing a multi-frame artifact (GLB exhibit, animated
  preview). `export_animated_glb` consumes the same factory.

Skip when the assembly's motion isn't relevant to the question
you're answering: pure static fit / printability runs faster on a
plain flat `Assembly`.

### Joint primitives

Today the library exposes one joint type:

```python
from build123d import Axis
from cad_khana.mechanism.assembly import RevoluteJoint

joint = RevoluteJoint(
    axis=Axis((px, py, pz), (dx, dy, dz)),   # in the PARENT'S frame
    angle_deg=0.0,                            # animatable DOF
)
```

The axis is interpreted in the frame of the parent `Assembly` that
owns the sub-assembly — not in the sub-assembly's local frame, and
not in world. `angle_deg` is the value the animation factory
updates per frame.

### Composing animated assemblies

A jointed sub-assembly is added with
`with_subassembly(name, sub, location=..., joint=...)`. The
sub-assembly is itself a full `Assembly` (it can contain parts,
sub-sub-assemblies, joints) — nest as deeply as the mechanism needs.
Reach into the tree with dotted paths:

```python
turret = (
    Assembly()
    .with_subassembly(
        "rotor",
        rotor_internals,                              # an Assembly
        location=Pos(0, 0, 0),
        joint=RevoluteJoint(axis=Axis.Z),             # parent's Z
    )
    .with_subassembly(
        "kicker_lever",
        lever_internals,
        joint=RevoluteJoint(axis=Axis((px, 0, pz), (0, -1, 0))),
    )
)
# later — animation hook:
turret = turret.with_joint_angle("rotor", 45.0)
turret = turret.with_joint_angle("rotor.platform_dump", 12.5)  # nested
```

`with_joint(path, joint)` is the alternative shape: attach (or
replace) the joint on an already-composed sub-assembly instead of
passing `joint=` at `with_subassembly` time. Same dotted-path form
as `with_joint_angle`; raises `KeyError` if any segment is missing.

`with_joint_angle` raises if the path doesn't reach a jointed
sub-assembly.

### The `t → Assembly` factory

A function `factory(t: float) -> Assembly` that returns the static
assembly at parameter `t` is the project's animation primitive.
Motion is expressed *in user code* as math (`angle = f(t)`); the
library samples `factory(t)` and emits glTF or runs per-frame
checks.

```python
def build_at(t: float) -> Assembly:
    a = build_static()
    a = a.with_joint_angle("rotor", 360.0 * t)
    a = a.with_joint_angle("kicker_lever", lift_schedule(t))
    return a
```

`cad_khana.export.export_animated_glb(factory, ts, out, ...)`
sweeps the factory over a sequence of `t` values, tessellates
geometry once from `factory(ts[0])`, and injects animation samplers
per jointed sub-assembly. Each jointed `with_subassembly(...)`
becomes one `animgroup_N` node in the GLB scene graph whose
childr

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cyberchitta](https://github.com/cyberchitta)
- **Source:** [cyberchitta/cad-khana](https://github.com/cyberchitta/cad-khana)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cyberchitta-cad-khana-cad-khana
- Seller: https://agentstack.voostack.com/s/cyberchitta
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
