atomcad
Interact with atomCAD node networks programmatically. Query, edit, and replace CAD geometry nodes for atomic/molecular structure design. Use when working with atomCAD projects or when the user wants t
SKILL.md.claude/skills/atomcad/skill.md
GitHubatomCAD Skill
Programmatically interact with atomCAD node networks via CLI. Requires atomCAD to be running.
Prerequisites
- atomCAD running (the CLI connects to a running instance)
CLI access (automatic detection):
- In atomCAD repo root:
./atomcad-cliworks directly (no PATH setup needed) - Elsewhere: requires
atomcad-clion PATH
Command Resolution
Before running CLI commands, detect the appropriate command:
- Development mode: If
./atomcad-cliexists in current directory → use./atomcad-cli - Installed mode: If
atomcad-cliis in PATH → useatomcad-cli - Not found: If neither works, inform the user they need to set up the CLI
Setup instructions (if CLI not found):
- Run the setup script from the atomCAD installation directory:
- Windows:
.\setup\setup-skill.bat - Linux/macOS:
bash setup/setup-skill.sh
- Windows:
- Or add the CLI manually to PATH (the CLI is in the
cli/folder of the atomCAD installation)
Core Concepts
Node Networks
atomCAD designs are parametric node networks (DAGs):
- Nodes have typed input pins (parameters) and one output pin
- Wires connect output→input of compatible types
- Evaluation is lazy: only visible nodes trigger computation
- Custom nodes are subnetworks with matching names
Each network can have an output node (set via output <node_id>) that defines what value the network returns when used as a custom node.
Lattice Coordinates
Important: Geometry coordinates (positions, sizes, radii) are in discrete lattice units (integers), not angstroms. The actual physical dimensions depend on the unit cell. This ensures all atomic structures are naturally lattice-aligned.
Unit Cell and Motif
- Default unit cell: Cubic diamond (3.567Å lattice constant) if none specified
- Default motif: Cubic zincblende with carbon atoms (pure diamond)
- Geometry nodes accept a
unit_cellinput to specify different lattices - Non-cubic unit cells cause shapes like
cuboidto become parallelepipeds
Data Types
| Type | Description |
|---|---|
Bool, String, Int, Float |
Primitives |
Vec2, Vec3, IVec2, IVec3 |
2D/3D vectors (float/int) |
Geometry2D |
2D shapes (for extrusion) |
Geometry |
3D geometry (SDF-based) |
Atomic |
Atomic structure (atoms + bonds) |
UnitCell |
Crystal lattice parameters |
Motif |
Crystal motif definition |
[T] |
Array of type T |
A -> B |
Function type |
Implicit conversions: Int↔Float, IVec↔Vec, T→[T]
Array pins (marked with dot) accept multiple wires; values are concatenated.
Text Format Syntax
# Set network description (single-line)
description "A parametric gear assembly"
# Or multi-line with triple quotes
description """
This network creates a parametric gear.
Parameters:
- teeth: number of teeth
- radius: outer radius
"""
# Create nodes: id = type { parameter: value, ... }
sphere1 = sphere { center: (0, 0, 0), radius: 5, visible: true }
cuboid1 = cuboid { min_corner: (-5, -5, -5), extent: (10, 10, 10) }
# Wire nodes by referencing node IDs as input values
union1 = union { shapes: [sphere1, cuboid1], visible: true }
# Set network output node
output union1
# Delete a node
delete sphere1
# Anchor a comment to what it documents: a node, or one wire
note1 = Comment { text: "the chassis", on: cuboid1 }
note2 = Comment { text: "the union branch", on: sphere1 -> union1.shapes }
Parameter values:
- Integers:
42,-10 - Floats:
3.14,1.5e-3 - Booleans:
true,false - Strings:
"hello"(single-line) or"""multi\nline"""(triple-quoted for multi-line) - Vectors:
(x, y)or(x, y, z) - Arrays:
[a, b, c] - Node references: use the node's ID
Note on vectors: The tuple syntax (x, y, z) is for literal values in node parameters (e.g., sphere { center: (5, 5, 5) }). The vector constructor nodes (vec2, vec3, ivec2, ivec3) use separate component inputs instead:
vec3 { x: 1.5, y: 2.5, z: 3.5 }
ivec3 { x: 1, y: 2, z: 3 }
Zone Bodies (map, filter, fold, foreach, zip_with, closure)
These node types carry an inline body — a nested network that runs once per
element (or, for closure, is exposed as a Function value). The body is
projected as a body { … } block in the node's braces, and it is the last
item in the property list:
r = range { start: 0, count: 5, step: 1 }
scale = int { value: 3 }
m1 = map {
xs: r,
input_type: Int,
output_type: Int,
body {
d = expr { a: $element, b: ^scale, expression: "a * b", parameters: [{ name: "a", data_type: Int }, { name: "b", data_type: Int }] }
e = expr { a: d, expression: "a + 1", parameters: [{ name: "a", data_type: Int }] }
output e
}
}
output m1
body { … }is a block, not a property value — there is no:afterbody, and the statements inside carry no commas.output einside the block feeds the HOF's zone-output pin.output m1outside it sets the network's return node. Same keyword; the position disambiguates.- A node carrying a body spans several lines. Consumers that assumed "one statement = one line" must brace-match.
- A node whose body is empty emits no block at all — that is the shape of an
HOF driven through its
f:pin. - A
body { … }block on a node type that has no body (i = int { value: 1, body { … } }) is refused, not ignored.
Referring outward from inside a body
Four spellings, and nothing else. With k = the number of leading ^:
k carets → a node k scopes out; a $ prefix → a zone input k + 1 frames
out.
| Written | Resolves to | Encoding |
|---|---|---|
n |
a node in this body | NodeOutput, depth 0 |
^n |
a node one scope out (a capture) | NodeOutput, depth 1 |
^^n |
a node two scopes out | NodeOutput, depth 2 |
$element |
this body's own HOF's iteration value | ZoneInput, depth 1 |
^$element |
the enclosing HOF's iteration value | ZoneInput, depth 2 |
^^$element |
two HOFs out | ZoneInput, depth 3 |
The + 1 is a real asymmetry, not a typo: ^n counts networks, while $name
already names this body's own owner, so reaching the enclosing HOF's element
needs one caret.
outer = map {
xs: rows,
body {
inner = map {
xs: $element,
body {
p = expr { a: $element, b: ^$element, expression: "a * b", parameters: [{ name: "a", data_type: Int }, { name: "b", data_type: Int }] }
output p
}
}
output inner
}
}
$element in the inner body is the inner map's element; ^$element is the
outer map's.
A bare name is resolved lexically — this body first, then outward — so
b: scale also works above. $name never searches outward: promoting a
per-iteration read into a capture would change evaluation semantics, not just
the referent. query always emits the explicit ^ form.
Zone pin names are not uniform
Only map and zip_with call the result result. Read them from this table,
or from atomcad-cli describe <node>:
| Node | Zone inputs ($…) |
Zone output (output …) |
|---|---|---|
map |
element |
result |
filter |
element |
keep (Bool) |
foreach |
element |
out (Unit) |
fold |
acc, element |
new_acc |
zip_with |
element1 … elementN |
result |
closure |
its own params |
per kind: new_acc (fold), out (foreach), else result |
closure nodes
A closure carries its own definition, and its zone inputs are named by its
params:
f1 = closure {
kind: "custom",
params: ["x", "y"],
type_args: [Int, Int, Int],
body {
p = expr { a: $x, b: $y, expression: "a * b", parameters: [{ name: "a", data_type: Int }, { name: "b", data_type: Int }] }
output p
}
}
kind—"map","filter","fold","foreach"or"custom"(quoted).params— the parameter names, and therefore what the body's$xbinds to. Only meaningful forkind: "custom"; the preset kinds name their own (element, oracc/elementforfold).type_args— the free type slots:[T, U]formap,[T]forfilterandforeach,[A, T]forfold,[p0, …, pN-1, R]forcustom.
Write these properties in any order relative to the block — the editor always
applies a statement's properties before its body, so $x binds correctly
even when params: comes after body { … }.
f: overrides body
Every HOF also has an optional f: function pin. When f is wired it
overrides the inline body — the body is ignored at evaluation. query emits
both when both exist, so a network can show a body { … } that never runs.
Don't author a body for a node whose f: you are also wiring.
Editing a body: a block replaces, a path merges
# Whole-body assign: m1's body becomes exactly what the block says.
m1 = map { xs: r, body { a = int { value: 1 } output a } }
# Path-addressed: merges into the existing body, siblings untouched.
m1/d = expr { a: $element, expression: "a * 4", parameters: [{ name: "a", data_type: Int }] }
m1/new1 = int { value: 7 } # add a node to m1's body
output m1/new1 # re-point m1's zone-output wire
delete m1/e # remove one body node
outer/inner/n1 = int { value: 2 } # depth 2
Rules that matter:
- A mentioned
body { … }block assigns the whole body, exactly like any other property. Omitbodyand the body is left alone. - A block with no
outputstatement clears the zone-output wire. The block is total over the parent's zone-output too — that is what makesquery→edit --replaceexact.body { }empties the body and clears the wire. - A path merges.
m1/x = …creates or updatesxand leaves every sibling's identity and canvas position alone. - The separator is
/, never.—.already means pin access (atom_edit.diff). - Inside a path statement everything is relative to the scope it lands on:
bare names resolve in that body,
$…are its zone inputs, and^…walks outward from it. Som1/x = …means exactly what the same statement means written insidem1's block. - A path never creates the HOF it addresses.
m1must already exist and own a body, or the statement is an error, not a silent no-op. - Ordering within one script is significant. A
bodyblock followed by a path statement for the same node applies in order: the block wipes, the path merges into the result. Don't straddle the two unless you mean to. - Rebuilt body nodes keep their identity by name, so an
edit --replaceof unchangedqueryoutput changes no ids and moves nothing on the canvas.
success means validates, not merely parsed
edit returns JSON. success: true means the script parsed, applied and
the resulting network validates. Blocking validation errors land in errors
(and fail the edit); non-blocking ones land in warnings. Names in
nodes_created / nodes_updated / nodes_deleted and in error text are full
paths (m1/a), because m1/a and m2/a are different nodes.
Two things a body edit commonly trips on, both reported this way:
- A body with no wire into its zone-output pin is a blocking error — a
freshly created HOF gets an empty body, so
m1 = map { xs: r }alone leavesm1invalid until you give it abodyor wire itsf:. - A
^depth that walks past the outermost scope is an error, not a silent drop.
Round-trip caveat: query output contains blank lines, and atomcad-cli edit reading from stdin stops at the first blank line. To feed query output
straight back, strip the blank lines, or pass it via --code with \n escapes.
Every edit is one undo step
Each edit call is recorded in the app's undo stack as a single step labelled
AI edit network, so the user can revert a whole edit — zone bodies included —
with Ctrl+Z, and reapply it with Ctrl+Y. An edit that applied but then failed
validation is still undoable. Tell the user this rather than trying to
reconstruct a previous state yourself; there is no CLI undo command.
CLI Commands
Global Options
atomcad-cli --help # Show help
atomcad-cli --port=PORT # Custom server port (default: 19847)
atomcad-cli --document=DOC <command> # Refuse unless DOC is the active tab
Tabs: always pass --document
atomCAD has tabs, one open design per tab, and every command acts on
whichever tab is active. The user can switch tabs at any time, so without
a guard an edit meant for one design can land in another. Pass
--document <path-or-id> on every command once you know which design
you are working on; a command naming anything but the active tab is refused
before it does anything, with a message naming the active one:
atomcad-cli documents # list the tabs: id, path, active (*), dirty
# * 2 C:/work/host.cnnd (modified)
# 3 Untitled
atomcad-cli --document C:/work/host.cnnd query # a saved design: its path
atomcad-cli edit --document 3 --code="s = sphere {}" # an Untitled one: its id
loadandnewprint the document id and path of the tab they opened; use it from then on.fileprints it too (Document: 2).--documentmay go before or after the command. A relative path is resolved against the CLI's working directory; any spelling of the path works.- When a command is refused, do not retry with the other document: the user switched tabs on purpose. Tell them, and ask whether to continue there or wait until they switch back.
load,newanddocumentsare not guarded (they do not act on the active tab).
Network Operations
# Query current network state
atomcad-cli query
# Edit network (add/update nodes, keeps existing)
atomcad-cli edit --code="sphere1 = sphere { radius: 10 }"
# Replace entire network (clears first)
atomcad-cli edit --code="..." --replace
# Multi-line edit (piped stdin is read to EOF, blank lines included;
# interactively, an empty line or "." sends)
atomcad-cli edit
atomcad-cli edit --replace
Query output format:
# Network: Main
description "A boolean union example"
sphere1 = sphere { center: (0, 0, 0), radius: 5 }
cuboid1 = cuboid { min_corner: (0, 0, 0), extent: (10, 10, 10) }
result = union { shapes: [sphere1, cuboid1], visible: true }
output result
# 3 nodes
The output includes:
- Header with active network name
- Description statement (if set)
- Node definitions in topological order
- Output statement (if set)
- Footer with node count
The header and footer use comment syntax (#), so the output is valid input to edit --replace.
Multi-line Input
For multi-line code, stdin piping is recommended as it avoids shell quoting issues:
# Recommended: pipe multi-line code via stdin
echo "base = cuboid { extent: (10, 10, 10) }
hole = sphere { center: (5, 5, 5), radius: 4 }
result = diff { base: [base], sub: [hole], visible: true }" | atomcad-cli edit --replace
# Or use a heredoc
atomcad-cli edit --replace <<'EOF'
base = cuboid { extent: (10, 10, 10) }
hole = sphere { center: (5, 5, 5), radius: 4 }
result = diff { base: [base], sub: [hole], visible: true }
EOF
Alternative: The --code flag supports \n escape sequences for newlines:
# Escape sequences: \n for newline, \t for tab, \\ for literal backslash
atomcad-cli edit --replace --code="base = cuboid { extent: (10, 10, 10) }\nhole = sphere { radius: 4 }\nresult = diff { base: [base], sub: [hole], visible: true }"
Note: A literal newline inside --code="..." works too (the wrappers and the CLI handle it), but shell quoting of multi-line strings varies across platforms, so stdin piping or the \n escape form is the safer habit.
Node Network Management
atomCAD designs can contain multiple node networks. Use these commands to manage them:
# List all node networks
atomcad-cli networks
# Create a new network
atomcad-cli networks add # Auto-named (UNTITLED, UNTITLED1, ...)
atomcad-cli networks add --name "my_shape" # Specific name
# Switch active network (query/edit will operate on this)
atomcad-cli networks activate <name>
# Delete a network
atomcad-cli networks delete <name>
# Rename a network
atomcad-cli networks rename <old> <new>
Output example for networks:
Node Networks:
* Main (active)
cube
pattern [ERROR: Invalid parameter type]
3 networks (1 with errors)
Behavior notes:
networks addauto-activates the new networknetworks deletefails if the network is referenced by other networksnetworks renameupdates all references in other networks automatically
Note: query and edit always operate on the active network.
Linked Libraries
A design can link other .cnnd files. A linked library's networks and
record types appear under its alias (demolib.half_space, demolib.Miller,
nested demolib.common.slab) and are used like local ones — but they are
read-only: edit on a linked network is refused, and networks marks
them (linked, read-only). query on one prints a # linked from <path>
line under the # Network: header. To change one, open the library file
(load <path> — it opens in its own tab, or switches to it), edit with
--document <library path>, save, then load the design's path again: that
switches back to the design's tab, which picks up the saved library on the
way.
atomcad-cli libraries # list, with status
atomcad-cli libraries link libs/demolib.cnnd demolib # path relative to the design
atomcad-cli libraries refresh [demolib] # re-read one, or all
atomcad-cli libraries unlink demolib # refused while used
atomcad-cli libraries rename demolib libs.demolib # renames every use too
atomcad-cli libraries make-local demolib # copy into the design, unlink
Each is one undo step. refresh prints one line per wire it disconnected.
Refer to a library's content by its full name in the text format, backticked
when it contains dots: f = `demolib.half_space` { … }.
File Operations
Load, save, and manage .cnnd project files. The application has tabs,
one open design per tab, and every command acts on the active tab:
# Open a .cnnd file in a new tab (or switch to its tab if already open)
atomcad-cli load design.cnnd
# Save current project
atomcad-cli save # Save to current file
atomcad-cli save design.cnnd # Save to new path (overwrites if exists)
atomcad-cli save other/design.cnnd --copy-deps # …and copy linked libraries / data files along
atomcad-cli save other/design.cnnd --no-deps # …or save the design alone
# Check file status
atomcad-cli file
# Create new project (in a new tab)
atomcad-cli new
Output examples:
# load success
Loaded: /path/to/design.cnnd (3 networks, document 2)
# load of a file already open in a tab
Already open; switched to its tab: /path/to/design.cnnd (3 networks, document 2)
# save success
Saved: /path/to/design.cnnd
# file status
File: /path/to/design.cnnd
Modified: yes
Networks: 3
# file status (no file loaded)
File: (none)
Modified: no
Networks: 1
Behavior notes:
loadandnewnever discard anything: they open a tab (an untouched empty Untitled tab is replaced).--forceis accepted and does nothing. The user may switch tabs too — pass--document(see Tabs above).savewithout a path saves to the current file; fails if no file is loadedsave <path>into another folder fails, listing the files, when the design reads libraries or data files by a relative path that would not be there; choose--copy-deps(existing different files are kept) or--no-depsnewopens a fresh design with one "Main" network in a new tab- Relative paths are resolved relative to the CLI's working directory
Evaluate Node Results
Evaluate a specific node and return its computed result (useful for verification and debugging).
# Evaluate a node by ID or custom name
atomcad-cli evaluate <node_id>
atomcad-cli evaluate sphere1
# Verbose output (detailed info for complex types)
atomcad-cli evaluate <node_id> --verbose
atomcad-cli evaluate atoms1 --verbose
Output formats:
- Primitives (Float, Int, Bool, String): Display the value directly (e.g.,
42,3.140000,true) - Vectors (Vec3, IVec3, etc.): Display coordinates (e.g.,
(5.000000, 10.000000, 3.000000)) - Geometry/Geometry2D: Display
GeometryorGeometry2D(brief), CSG tree details (verbose) - Atomic: Display
Atomic(brief), or atom/bond counts and sample atoms (verbose)
Examples:
$ atomcad-cli evaluate sphere1
Geometry
$ atomcad-cli evaluate count1
42
$ atomcad-cli evaluate pos1
(5.000000, 10.000000, 3.000000)
$ atomcad-cli evaluate atoms1 --verbose
Atomic:
atoms: 968
bonds: 1372
first 10 atoms:
[1] Z=6 pos=(-6.242250, -6.242250, -4.458750) bonds=4
...
Run a chemisorb search
chemisorb is the one node whose result evaluation never computes: query and
evaluate only ever see its plan (stats.searched = false, no candidates).
The search runs on an explicit action — the panel's Run button, or:
atomcad-cli run <node> # by custom name or id, active network
atomcad-cli run mount
It prints a summary (hypotheses relaxed, candidates listed, best score and bond
inventory, a warning if the budget truncated the search). The result is stored on
the node and output until an input or a setting changes; it is not saved with
the file, so after load run again. Seconds to minutes.
H transfers (OH legs handing their H to the surface; to_substrate only) are
enabled through the optional transfers pin — note the Record(Name) spelling
of the element type:
h_off = array { element_type: Record(ChemisorbTransfer), elements: [{ element: 1, direction: "to_substrate" }] }
mount = chemisorb { adsorbate: tool, substrate: surface, adsorbate_tag: "feet", transfers: h_off, formed_bonds: 3 }
To see what the search did at one step, select an item of its search tree —
the same as clicking it in the panel. The tree alternates states (the root,
a leg: the bonds made so far) and next feet (one foot's test from a state:
the foot, the sites it accepted, the test's shape). The item shows on the
node's debug (pin 2, the structure with its markings) and debug_shapes
(pin 3, a next foot's test shape); --show displays both, then take a
screenshot:
atomcad-cli debug-select mount # the root: the posed inputs
atomcad-cli debug-select mount 12 --show # next foot 12: its anchor sphere
atomcad-cli debug-select mount 12-45 # one leg bonded, seated
atomcad-cli debug-select mount 12-45,13 # next foot 13 after it: the shell
atomcad-cli debug-select mount 12-45,13-61,14 # next foot 14: the leg-3 ring
atomcad-cli debug-select mount 12-45,13-61 --form seated # after run
A path is the legs as foot-site atom-id pairs (the ids of a candidate's sites
field), optionally followed by a foot id alone for that next foot, or #N for
tree row N. The command prints what the item marks (bonded, the foot, accepted
sites, clashing, near misses with how far they missed) and its children's
paths. A relaxed row that is not a listed candidate is re-relaxed first, so it
can take a few seconds.
Node Discovery
# List all node types by category
atomcad-cli nodes
# List nodes in specific category
atomcad-cli nodes --category=Geometry3D
# List with descriptions (verbose mode, can combine with --category)
atomcad-cli nodes --verbose
# Get detailed info about any node type (built-in or custom)
atomcad-cli describe <node-name>
atomcad-cli describe sphere
atomcad-cli describe atom_fill
Use describe to discover input pins, types, defaults, and behavior for any node.
Camera Control
Control the viewport camera position and projection mode.
# Get current camera state (returns JSON)
atomcad-cli camera
# Set camera position and orientation
atomcad-cli camera --eye x,y,z --target x,y,z --up x,y,z
# Switch projection mode
atomcad-cli camera --orthographic # Orthographic projection (no perspective)
atomcad-cli camera --perspective # Perspective projection (default)
# Set orthographic zoom level (half-height of viewport)
atomcad-cli camera --ortho-height 50
# Combined example: position camera and switch to orthographic
atomcad-cli camera --eye 30,30,30 --target 0,0,0 --up 0,0,1 --orthographic
Important: Camera coordinates are in angstroms, while geometry node coordinates are in lattice units. For cubic diamond (default), 1 lattice unit ≈ 3.567 Å. A 20×20×20 lattice unit object is ~71 Å across, so position the camera 150-250 Å away to see the full object.
Parameters:
--eye x,y,z— Camera position in angstroms (world coordinates)--target x,y,z— Point the camera looks at (angstroms)--up x,y,z— Up vector for camera orientation (typically0,0,1)--orthographic— Enable orthographic projection (parallel rays, no perspective distortion)--perspective— Enable perspective projection (default, realistic depth)--ortho-height N— Half-height of orthographic viewport (controls zoom level)
Response: Returns JSON with current camera state:
{
"success": true,
"camera": {
"eye": [30.0, 30.0, 30.0],
"target": [0.0, 0.0, 0.0],
"up": [0.0, 0.0, 1.0],
"orthographic": true,
"ortho_half_height": 25.0
}
}
Display Settings
Control viewport display preferences including atomic visualization and geometry rendering.
# Get current display settings (returns JSON)
atomcad-cli display
# Set atomic visualization mode
atomcad-cli display --atomic-viz ball-and-stick # Small atoms with bond sticks
atomcad-cli display --atomic-viz space-filling # Large van der Waals spheres
# Set geometry visualization mode
atomcad-cli display --geometry-viz surface-splatting # Implicit SDF rendering
atomcad-cli display --geometry-viz solid # Solid mesh (default)
atomcad-cli display --geometry-viz wireframe # Wireframe mesh
# Set node display policy
atomcad-cli display --node-policy manual # User controls visibility
atomcad-cli display --node-policy prefer-selected # Show selected node (default)
atomcad-cli display --node-policy prefer-frontier # Show output nodes
# Set background color
atomcad-cli display --background 30,30,30 # Dark gray (default)
atomcad-cli display --background 255,255,255 # White
# Combine multiple settings
atomcad-cli display --atomic-viz space-filling --geometry-viz wireframe
Parameters:
--atomic-viz <mode>— Atomic visualization:ball-and-stick(default) orspace-filling--geometry-viz <mode>— Geometry visualization:solid(default),wireframe, orsurface-splatting--node-policy <policy>— Node display:prefer-selected(default),manual, orprefer-frontier--background R,G,B— Background color as RGB values 0-255 (default: 30,30,30)
Response: Returns JSON with current display state:
{
"success": true,
"display": {
"atomic_visualization": "ball-and-stick",
"geometry_visualization": "solid",
"node_display_policy": "prefer-selected",
"background_color": [30, 30, 30]
}
}
Screenshot Capture
Capture the current viewport to a PNG image file.
# Capture with default resolution (current viewport size)
atomcad-cli screenshot -o output.png
# Auto-generate timestamped filename (screenshot_YYYY-MM-DD_HHMMSS.png)
atomcad-cli screenshot -o auto
# Capture with specific resolution
atomcad-cli screenshot -o output.png -w 1920 -h 1080
# Capture with custom background color (R,G,B values 0-255)
atomcad-cli screenshot -o output.png --background 255,255,255
Parameters:
-o, --output <path>— Output PNG file path (required). Useautoto generate a timestamped filename-w, --width N— Image width in pixels (optional, uses viewport size if not specified)-h, --height N— Image height in pixels (optional, uses viewport size if not specified)--background R,G,B— Background color as RGB values 0-255 (default: 30,30,30 dark gray)
Response: Returns confirmation with file path and actual dimensions:
Screenshot saved: /path/to/output.png (1920x1080)
Note: Relative paths are resolved relative to the CLI's working directory, not atomCAD's.
Always verify screenshots: After capturing, read the PNG file to check the result. If the view is too close (only atoms visible, no overall shape) or too far (object too small), adjust the camera and retake. Common issue: using small camera coordinates puts you inside the atomic structure. If the entire screenshot appears green, you are too close to the objects (geometry is rendered in green).
Recommended visualization styles for attractive screenshots:
- Ball-and-stick with visible geometry: Use
--atomic-viz ball-and-stickand make the geometry node feeding intoatom_fillvisible (visible: true). This shows atoms running along the surface of the green geometry shape—a visually striking combination. - Space-filling only: Use
--atomic-viz space-fillingand hide geometry nodes (visible: false). The large van der Waals spheres will mostly occlude any geometry anyway, so keeping geometry hidden produces cleaner results.
Typical AI Agent Workflow
Combine geometry creation, camera control, display settings, and screenshots for visual verification:
# 1. Create geometry
atomcad-cli edit --code="s = sphere { radius: 10, visible: true }"
# 2. Position camera for good viewing angle
atomcad-cli camera --eye 30,30,30 --target 0,0,0 --up 0,0,1 --orthographic
# 3. Adjust display settings for clear visualization
atomcad-cli display --atomic-viz space-filling --background 255,255,255
# 4. Capture screenshot for visual verification
atomcad-cli screenshot -o sphere_check.png
# 5. Verify result by viewing the image
# (The AI agent can read the PNG file to see the rendered geometry)
REPL Mode
atomcad-cli # Enter interactive mode
Commands:
query/q— Show current node networkedit— Enter edit mode (incremental)replace/r— Enter edit mode (replace entire network)evaluate/e <node>— Evaluate a noderun <node>— Run achemisorbnode's searchdebug-select <node> [<path>] [posed|seated|relaxed] [show]— Show achemisorbsearch-tree item on its debug pinsnodes— List available node typesdescribe/d <node>— Describe a node typenetworks— List all node networksnetworks add [--name X]— Create new networknetworks delete <name>— Delete a networknetworks activate <name>— Switch to a networknetworks rename <old> <new>— Rename a networkcamera/c— Get/set camera statedisplay— Get/set display preferencesscreenshot/s <path>— Capture viewport to PNGload <path>— Open a .cnnd file in a tabsave [path]— Save to filefile— Show current file statusnew— New project in a new tabhelp/?— Show helpquit/exit— Exit REPL
Common Patterns
Create a simple atomic structure
# Create sphere geometry and fill with atoms
atomcad-cli edit --code="s = sphere { radius: 5, visible: true }"
atomcad-cli edit --code="atoms = atom_fill { shape: s, visible: true }"
Boolean operations on geometry
# Using heredoc (recommended for multi-line)
atomcad-cli edit --replace <<'EOF'
base = cuboid { extent: (10, 10, 10), visible: false }
hole = sphere { center: (5, 5, 5), radius: 4 }
result = diff { base: [base], sub: [hole], visible: true }
EOF
# Or using escape sequences
atomcad-cli edit --replace --code="base = cuboid { extent: (10, 10, 10), visible: false }\nhole = sphere { center: (5, 5, 5), radius: 4 }\nresult = diff { base: [base], sub: [hole], visible: true }"
Note: diff and diff_2d accept arrays on both inputs. They implicitly union each array before computing the difference: diff(base, sub) = base₁ ∪ base₂ ∪ ... − (sub₁ ∪ sub₂ ∪ ...)
Editing individual atoms with atom_edit
The atom_edit node applies a diff (additions, deletions, replacements, moves) to an input atomic structure. Edits are specified via a line-based text format in the diff property.
# Create a silicon crystal and edit it (T-center defect)
atomcad-cli edit --replace <<'EOF'
geom = cuboid { extent: (2, 2, 2), visible: false }
atoms = atom_fill { shape: geom, parameter_element_value_definition: "PRIMARY Si\nSECONDARY Si", rm_single: true, visible: false }
edited = atom_edit { molecule: atoms, diff: """# Replace two Si with C, add bridging H
~C @ (0.89175, 2.67525, 2.67525)
~C @ (1.7835, 1.7835, 3.567)
+H @ (1.337625, 2.229375, 3.121125)
bond 1-3 single
bond 2-3 single""", visible: true }
EOF
Diff format — atom lines:
+El @ (x, y, z)— Add a new atom (e.g.,+H @ (1.0, 2.0, 3.0))~El @ (x, y, z)— Replace the base atom at this position with element El (e.g.,~C @ (0.89, 2.67, 2.67)replaces Si with C)~El @ (x, y, z) [from (ox, oy, oz)]— Move atom: match base atom at anchor position, place at new position- @ (x, y, z)— Delete the base atom at this position
Diff format — bond lines (1-based atom indices referencing atom lines above):
bond A-B order— Add bond (orders: single, double, triple, quadruple, aromatic, dative, metallic)unbond A-B— Delete bond between atoms A and B
Notes:
- Positions are in angstroms (real-space coordinates), not lattice units
- Element symbols are standard (C, Si, N, O, H, Fe, etc.) and case-insensitive
- The
~prefix means the atom is expected to match a base atom (replacement or move). The+prefix means a new atom addition. Both use positional matching internally, but~preserves user intent on round-trip. - Deletion matches atoms in the input structure by position (within tolerance)
- Lines starting with
#are comments. Blank lines are ignored.
Multi-line diff with heredoc:
atomcad-cli edit <<'EOF'
edited = atom_edit { molecule: atoms, diff: """
+C @ (0.0, 0.0, 10.0)
+C @ (0.0, 0.0, 13.567)
bond 1-2 single
""", visible: true }
EOF
atom_edit options:
output_diff: true— Show the diff itself instead of the applied result (for debugging/visualization)show_anchor_arrows: true— When viewing diff, show arrows from anchor positions to moved atomsbase_bonds: true— Include base bonds between matched diff atoms in diff outputtolerance: 0.1— Positional matching tolerance in angstroms (default: 0.1)
Creating realistic atomic structures
# Passivate dangling bonds with hydrogen
atomcad-cli edit --code="atoms = atom_fill { shape: geom, passivate: true, visible: true }"
# Remove single-bond atoms recursively (cleaner structures)
atomcad-cli edit --code="atoms = atom_fill { shape: geom, rm_single: true, visible: true }"
# Override motif elements (e.g., silicon carbide instead of diamond)
atomcad-cli edit --code="atoms = atom_fill { shape: geom, parameter_element_value_definition: \"PRIMARY Si\\nSECONDARY C\", visible: true }"
atom_fill options:
passivate: true— Add hydrogen atoms to dangling bondsrm_single: true— Recursively remove atoms with only one bondsurf_recon: true— Enable surface reconstruction (cubic diamond (100) 2×1 only)parameter_element_value_definition: "..."— Override motif parameter elements (newline-separatedPARAM_NAME Element, e.g.,"PRIMARY Si\nSECONDARY Si"for pure silicon)m_offset: (x, y, z)— Fractional offset (0-1) to adjust cut position
Parametric design with custom nodes
Custom nodes are created by defining subnetworks:
- Create a node network named
my_shapeusingnetworks add --name "my_shape" - Add
parameternodes to define inputs (each parameter becomes an input pin) - Set the network's output node via
output <node_id> - Switch to another network and use
my_shapeas a node type
# Create a custom node (subnetwork) for a parameterized shape
atomcad-cli networks add --name "rounded_cube"
atomcad-cli edit --replace <<'EOF'
size = parameter { name: "size", type: Int, default: 10 }
radius = parameter { name: "corner_radius", type: Int, default: 2 }
base = cuboid { extent: (size, size, size) }
corner = sphere { radius: radius }
result = intersect { shapes: [base, corner] }
output result
EOF
# Switch to Main and use the custom node
atomcad-cli networks activate "Main"
atomcad-cli edit --code="part = rounded_cube { size: 20, corner_radius: 3, visible: true }"
# Fill with atoms
atomcad-cli edit --code="atoms = atom_fill { shape: part, passivate: true, visible: true }"
Simpler example:
# Create a reusable scaled sphere node
atomcad-cli networks add --name "scaled_sphere"
atomcad-cli edit --replace <<'EOF'
size = parameter { name: "size", type: "Int", default: 5 }
s = sphere { radius: size }
output s
EOF
# Use it in Main
atomcad-cli networks activate "Main"
atomcad-cli edit --code="part1 = scaled_sphere { size: 10, visible: true }"
Functional programming with map
The map node applies a function to each array element. Combined with partial application:
# range creates [0, 1, 2, ...] array
# pattern is a custom node with inputs: index (Int), gap (Int)
# When wired to map's f input, gap is bound; only index varies
atomcad-cli edit <<'EOF'
r = range { start: 0, count: 5, step: 1 }
result = map { xs: r, f: pattern, gap: 3, visible: true }
EOF
Extra parameters beyond the expected function signature are bound at wire-time (partial application), enabling parametric patterns.
Mathematical expressions with expr
The expr node evaluates mathematical expressions with dynamic input pins.
Important: The parameters property defines which input pins the expression uses. Each parameter needs a name and data_type. The default expr node only has one parameter named x of type Int.
# Simple case: using the default 'x' parameter
atomcad-cli edit <<'EOF'
val = int { value: 5 }
doubled = expr { expression: "x * 2", x: val }
EOF
# Multiple inputs: must define parameters explicitly
atomcad-cli edit <<'EOF'
a = int { value: 5 }
b = int { value: 3 }
sum = expr {
expression: "x + y",
parameters: [{ name: "x", data_type: Int }, { name: "y", data_type: Int }],
x: a,
y: b
}
EOF
Available data types for parameters: Int, Float, Bool, Vec2, Vec3, IVec2, IVec3
Supported in expr:
- Arithmetic:
+,-,*,/,%,^(exponent) - Comparisons:
==,!=,<,<=,>,>= - Logic:
&&,||, ! - Conditionals:
if condition then value1 else value2 - Vectors:
vec2(x,y),vec3(x,y,z),ivec2,ivec3, member access.x,.y,.z - Functions:
sin,cos,tan,asin,acos,atan,atan2,sindeg,cosdeg,tandeg,asindeg,acosdeg,atandeg,atan2deg,degrees,radians,sqrt,exp,log,abs,min,max,clamp,sign,lerp,floor,ceil,round,length2,length3,normalize2,normalize3,dot2,dot3,cross,distance2,distance3min/maxare variadic (≥2 args);min/max/clamp/sign/absare type-preserving (all-Int args → Int result, else Float).abs_intis a deprecated Int-only alias ofabs.logis natural log;lerp(a,b,t)is unclamped and returns Float.- Classic trig (
sin/asin/atan2/…) works in radians; thedeg-suffixed variants (sindeg/asindeg/atan2deg/…) take/return degrees.degrees(x)/radians(x)convert between them.
- Constants:
pi(Float; a same-named parameter shadows it)
Important Notes
- facet_shell: Currently only works correctly with cubic unit cells
- lattice_move/lattice_rot: Discrete lattice transformations (integers only). For continuous transforms on atomic structures, use
atom_transinstead:atoms = atom_fill { shape: geom, visible: false } moved = atom_trans { molecule: atoms, translation: (10.0, 0.0, 0.0), visible: true } - half_space: Creates infinite half-spaces; useful for clipping geometry via intersection
- Lone atoms:
atom_fillautomatically removes atoms with zero bonds after the geometry cut
See Also
atomcad-cli describe <node>for detailed node documentationatomcad-cli nodesto browse available node typesreferences/text-format.mdfor complete text format specificationreferences/data-types.mdfor detailed type system documentation