Install
$ agentstack add skill-cyberchitta-cad-khana-cad-khana ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
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:
[
{
"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:
- Parameters + derived — named constants at the top, so one change
propagates through everything.
- Pure part functions — each returns a
Part. Take parameters with
defaults; no hidden globals, no mutation.
- Assembly composition — build an
Assemblyby chaining.with_part()
and .assert_*() calls. Call check(assembly, out="outputs").
- 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.
- Declare parts as pure functions. One function per distinct
printed body, taking parameters with defaults. No globals, no placement inside the function.
- Wire the assembly with explicit
Locations. Compose with
Assembly().with_part(name, part(), location=…). Names are stable IDs the assertions and diagnostics reference.
- **Add
assert_no_interferencebetween 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.
- **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.
- Run
khana checkand iterate until all scalars are green.
Reading mechanism.json is the primary loop; do not draw yet.
- Then
khana drawfor 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
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:
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:
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:
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.
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
- Source: cyberchitta/cad-khana
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.