docs: agent skills for modeling and editing through MCP (#728)
Two agent skills for modeling and editing through the MCP server, with a command reference and model-only screenshot examples.
This commit is contained in:
1 parent
293d660d6c
commit
be55a49b04
6 files changed
+464
No files matched your search
@@ -622,6 +622,117 @@ fn compact_execute_uses_one_backend_call_and_keeps_removed_bodies() {
|
||||
assert_eq!(v, json!({"feature": 7, "name": "Combine1", "created_bodies": [], "changed_bodies": ["Base"], "removed_bodies": ["Tool"]}));
|
||||
}
|
||||
|
||||
/// The worked example in `skills/solidcraft-modeling/reference.md` is what agents copy, so it has
|
||||
/// to keep building with the volume the file documents, and follow a parameter change.
|
||||
#[test]
|
||||
fn skill_reference_example_builds_as_documented() {
|
||||
let doc = include_str!("../../../skills/solidcraft-modeling/reference.md");
|
||||
let start = doc.find("{\"commands\":[").expect("the reference has a worked example");
|
||||
let end = start + doc[start..].find("```").expect("the example's code block ends");
|
||||
let example: Value = serde_json::from_str(&doc[start..end]).expect("the example is JSON");
|
||||
let mut s = server();
|
||||
tool(&mut s, "batch", example);
|
||||
// Plate with four r5 corners, minus four counterbored holes (Ø3.4 through, Ø6.5 × 3.3).
|
||||
let expected = |l: f64| {
|
||||
let (w, t) = (50.0, 6.0);
|
||||
let plate = l * w * t - 4.0 * 25.0 * (1.0 - PI / 4.0) * t;
|
||||
let hole = PI * 1.7 * 1.7 * (t - 3.3) + PI * 3.25 * 3.25 * 3.3;
|
||||
plate - 4.0 * hole
|
||||
};
|
||||
assert!((expected(80.0) - 23335.2).abs() < 0.05, "the reference states {}", expected(80.0));
|
||||
// Volume can't see where the holes are: each counterbore floor (z = T - 3.3) is centred on its
|
||||
// hole, at (±(L/2 - inset), ±(W/2 - inset)).
|
||||
let holes_at = |s: &mut Server, x: f64| {
|
||||
let topo = tool(s, "body_topology", json!({"body": "Body1"}));
|
||||
let mut floors: Vec<(i64, i64)> = topo["faces"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|f| f["centroid"].as_array())
|
||||
.filter(|c| (c[2].as_f64().unwrap() - 2.7).abs() < 1e-6)
|
||||
.map(|c| ((c[0].as_f64().unwrap() * 1000.0).round() as i64, (c[1].as_f64().unwrap() * 1000.0).round() as i64))
|
||||
.collect();
|
||||
floors.sort_unstable();
|
||||
let x = (x * 1000.0).round() as i64;
|
||||
assert_eq!(floors, vec![(-x, -17000), (-x, 17000), (x, -17000), (x, 17000)], "{topo}");
|
||||
};
|
||||
let m = tool(&mut s, "measure", json!({}));
|
||||
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap(), expected(80.0)) < 1e-5, "{m}");
|
||||
assert_eq!(m["bodies"][0]["faces"], json!(22), "{m}");
|
||||
holes_at(&mut s, 32.0);
|
||||
let r = tool(&mut s, "set_parameter", json!({"name": "L", "value": "120 mm"}));
|
||||
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
|
||||
assert!(rel(volume(&mut s), expected(120.0)) < 1e-5);
|
||||
holes_at(&mut s, 52.0);
|
||||
}
|
||||
|
||||
/// The skill reference's sketch-plane table: where sketch (10, 20) lands and which way +distance goes.
|
||||
#[test]
|
||||
fn sketch_plane_axes_match_the_skill_reference() {
|
||||
let mut s = server();
|
||||
for plane in ["XY", "XZ", "YZ"] {
|
||||
tool(
|
||||
&mut s,
|
||||
"batch",
|
||||
json!({"commands": [
|
||||
{"command": "sketch.create", "params": {"plane": plane}},
|
||||
{"command": "sketch.circle.center", "params": {"center": [10, 20], "radius": 1}},
|
||||
{"command": "sketch.finish"},
|
||||
{"command": "solid.extrude", "params": {"distance": 3, "operation": "new"}},
|
||||
]}),
|
||||
);
|
||||
}
|
||||
let m = tool(&mut s, "measure", json!({}));
|
||||
let centres: Vec<Vec<f64>> = m["bodies"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|b| b["center_of_mass"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect())
|
||||
.collect();
|
||||
let expected = [[10.0, 20.0, 1.5], [10.0, 1.5, -20.0], [1.5, 10.0, 20.0]];
|
||||
assert_eq!(centres.len(), 3, "{m}");
|
||||
for (got, want) in centres.iter().zip(expected) {
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-6), "{got:?} != {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every tool and command the agent skills call exists, so a rename or removal can't leave the
|
||||
/// skills sending agents to names the server no longer has.
|
||||
#[test]
|
||||
fn skills_name_only_real_tools_and_commands() {
|
||||
let docs = [
|
||||
include_str!("../../../skills/solidcraft-modeling/SKILL.md"),
|
||||
include_str!("../../../skills/solidcraft-modeling/reference.md"),
|
||||
include_str!("../../../skills/solidcraft-editing/SKILL.md"),
|
||||
];
|
||||
let mut s = server();
|
||||
let tools: Vec<String> = tool_definitions().iter().filter_map(|t| t["name"].as_str()).map(str::to_owned).collect();
|
||||
let commands: Vec<String> =
|
||||
tool(&mut s, "list_commands", json!({})).as_array().unwrap().iter().filter_map(|c| c["id"].as_str()).map(str::to_owned).collect();
|
||||
let mut checked = 0;
|
||||
for doc in docs {
|
||||
// A call in backticks is `name {}`, `name {"json":…}` or `name {placeholder}`: a tool when the
|
||||
// name is snake_case, a command when it is a dotted id. Field lists such as
|
||||
// `sides {bottom,right,top,left}` and `created: {…}` aren't calls. JSON examples name
|
||||
// commands as "command":"id".
|
||||
let ident = |w: &str| !w.is_empty() && w.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.');
|
||||
let calls = doc.split('`').skip(1).step_by(2).filter_map(|span| {
|
||||
let (head, args) = span.split_once(" {")?;
|
||||
let name = head.rsplit(' ').next()?;
|
||||
let args = args.strip_suffix('}')?;
|
||||
let call_like = args.is_empty() || args.starts_with('"') || args.chars().all(|c| c.is_ascii_lowercase() || c == '_');
|
||||
(ident(name) && call_like).then_some(name)
|
||||
});
|
||||
let in_json = doc.split("\"command\":\"").skip(1).filter_map(|rest| rest.split('"').next());
|
||||
for name in calls.chain(in_json) {
|
||||
let known = if name.contains('.') { commands.iter().any(|c| c == name) } else { tools.iter().any(|t| t == name) };
|
||||
assert!(known, "a skill calls `{name}`, which is neither a tool nor a command");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked > 30, "only {checked} tool and command names found in the skills");
|
||||
}
|
||||
|
||||
/// Every tool the server instructions name (a bare `snake_case` word in backticks) is a real tool.
|
||||
#[test]
|
||||
fn instructions_name_only_real_tools() {
|
||||
|
||||
@@ -45,6 +45,14 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app:
|
||||
}
|
||||
```
|
||||
|
||||
### Agent skills
|
||||
|
||||
[`skills/`](https://github.com/bherbruck/solidcraft/tree/main/skills) holds two [Agent Skills](https://agentskills.io) for agents that model through this server:
|
||||
- `solidcraft-modeling` covers building a new part.
|
||||
- `solidcraft-editing` covers changing an existing design.
|
||||
|
||||
They cover the workflow, the command shapes, how to verify with `measure`, and the known traps. With them an agent builds a part in a few `batch` calls and reads far less output. To install them for Claude Code, copy both folders into `~/.claude/skills/`; [skills/README.md](https://github.com/bherbruck/solidcraft/blob/main/skills/README.md) has the details.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Arguments | What it does |
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent skills
|
||||
|
||||
Skills for AI agents that model in SolidCraft through its [MCP server](../docs/mcp.md). Each folder is
|
||||
one skill in the [Agent Skills](https://agentskills.io) format: a `SKILL.md` with a name and a
|
||||
description that tell the agent when to load it, and an optional supporting reference file.
|
||||
|
||||
| Skill | Loads when the agent |
|
||||
|---|---|
|
||||
| [`solidcraft-modeling`](solidcraft-modeling/SKILL.md) | builds a new part (enclosure, bracket, plate, flange, …) |
|
||||
| [`solidcraft-editing`](solidcraft-editing/SKILL.md) | changes, fixes or extends an existing design |
|
||||
|
||||
They teach the agent to:
|
||||
- plan the part as numbers
|
||||
- build it in a few `batch` calls with parametric positions
|
||||
- verify every stage with `measure` against values it computed itself
|
||||
- skip the large replies it doesn't need, such as the unfiltered command catalog and full topology dumps
|
||||
|
||||
They also list the traps that otherwise cost failed calls. Examples are the sketch axes of the XZ plane, edge picks that snap to the nearest edge, and cuts that miss but still report success.
|
||||
|
||||
They are written for SolidCraft 0.8 and later.
|
||||
|
||||
## Install
|
||||
|
||||
Claude Code, for all projects:
|
||||
|
||||
```sh
|
||||
git clone --depth 1 https://github.com/bherbruck/solidcraft
|
||||
cp -r solidcraft/skills/solidcraft-* ~/.claude/skills/
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
git clone --depth 1 https://github.com/bherbruck/solidcraft
|
||||
Copy-Item -Recurse solidcraft\skills\solidcraft-* $HOME\.claude\skills\
|
||||
```
|
||||
|
||||
- **One project only:** copy the folders into that project's `.claude/skills/` instead.
|
||||
- **Other agents:** for agents that read the Agent Skills format, copy the folders into the skills directory their documentation names.
|
||||
- **Updating:** copy the folders again after you update SolidCraft.
|
||||
|
||||
## Maintaining
|
||||
|
||||
- The worked example in `solidcraft-modeling/reference.md` runs in CI. `skill_reference_example_builds_as_documented` in `crates/mcp/src/tests.rs` builds it, checks the documented volume, changes a parameter and checks it again.
|
||||
- The sketch-plane table is covered by `sketch_plane_axes_match_the_skill_reference` in the same file.
|
||||
- `skills_name_only_real_tools_and_commands` checks that every tool and command the skills call exists.
|
||||
- When a command, a parameter name or a behaviour that these files describe changes, update them in the same PR.
|
||||
- Facts that describe current limitations, such as edge picks without a distance limit, should go once the limitation is fixed.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: solidcraft-editing
|
||||
description: Use when changing, fixing or extending an existing SolidCraft design through its MCP server - a .solidcraft file or the design open in the app. Typical requests: resize it, move a cut, change a hole or standoff, add a fillet or chamfer, make a fixed value parametric, or a feature shows an error after a change or after the app crashed.
|
||||
---
|
||||
|
||||
# SolidCraft editing
|
||||
|
||||
## Overview
|
||||
Edit an existing design through its parameters instead of rebuilding it. Every sketch dimension and feature input is already a parameter (`d1`, `d2`, ...), so most edits are a `parameters.change` each, sent together in one batch. Map the design with a few compact calls rather than walking the timeline one feature at a time. For command shapes and traps, use `reference.md` from the `solidcraft-modeling` skill.
|
||||
|
||||
## Map the design (3-4 calls)
|
||||
1. **Open:** if the design is not open yet, run `open {path}`. When the server is bridged to the app and the current design is `dirty`, ask before opening anything.
|
||||
2. **Overview:** `inspect_design {"measure":false,"sketches":false}` lists the user parameters, the sketch dimensions (`d1`, `d2`, ... with their expressions), the features in order, and their errors.
|
||||
3. **Feature inputs:** `execute parameters.list {"filter":"feature"}` gives every feature input as an object such as `{"name":"d9","expression":"(6) / 2","feature":"Cylinder1","input":"Radius"}`. `inspect_design` leaves these out.
|
||||
- Match sketch dimensions by expression.
|
||||
- If that is ambiguous, `sketch.inspect {sketch}` that one sketch, which also shows its points in world coordinates.
|
||||
4. **Baseline:** `measure {}`, so you can predict every change against it.
|
||||
|
||||
`timeline.inspect {feature}` is only for two cases:
|
||||
- changing more than a number: profiles, direction, operation or edges
|
||||
- checking whether a position follows a parameter, for example a cylinder `base` written as `width/2 - wall - 6`
|
||||
|
||||
Inspect one feature of each kind, not every copy.
|
||||
|
||||
## Change
|
||||
- **A number:** `parameters.change {"name":"d9","expression":"..."}`.
|
||||
- **To make a fixed value parametric:** add a user parameter first (`parameters.add standoff_d`), then set each input to an expression of it (`standoff_d/2`).
|
||||
- **Several changes:** one `batch`.
|
||||
- **A user parameter:** `set_parameter` or `parameters.change`. Every expression that uses it follows.
|
||||
- **Structure:** `timeline.edit {"feature":..., "set":{...}}`, with the field shapes `timeline.inspect` shows.
|
||||
- **A new feature** goes at the end of the timeline. To place it earlier:
|
||||
1. `timeline.roll_to {"position":N}`. A position is safer than a name, since a sketch and a feature can share one.
|
||||
2. Add the feature.
|
||||
3. `timeline.roll_to {}` to return to the end.
|
||||
|
||||
Everything after the insert recomputes, and a feature that worked can now fail. Seen: a corner fillet after an inserted chamfer.
|
||||
- **Risky or multi-step edits:** take `edit.checkpoint` in its own call first. `edit.restore_checkpoint {checkpoint}` goes back exactly. A `batch` rolls itself back; separate `execute` calls do not.
|
||||
- **Deleting:** `timeline.dependents {feature}` first. It lists what is deleted with it and what would fail.
|
||||
- **Ambiguous wording:** "2 mm higher" on a value written as `wall + 6` can be relative (`wall + 8`) or absolute.
|
||||
- Keep the design's intent, which is the relative reading.
|
||||
- Tell the user which reading you used. This matters most when the same request also changes `wall`, because then the two readings give different heights.
|
||||
- **New constants** such as a chamfer size go in user parameters, so the user can change them later.
|
||||
|
||||
## Verify
|
||||
1. **Predict, then measure once.** Send the independent changes in one batch, predict the total volume change, then run `measure` once.
|
||||
- A move does not change the volume. To confirm a moved sketch, read its points' world coordinates with `sketch.inspect`. You can also predict the COM shift, `Δcom = V_moved · Δz / V_body`, but only when nothing else in that batch changes the volume.
|
||||
- If it matches to about 0.01%, you are done.
|
||||
- If not, restore the checkpoint, then apply the changes one at a time, measuring after each.
|
||||
- A cut or chamfer that leaves the volume unchanged has missed.
|
||||
2. **Timeline health:** run `inspect_design {"measure":false,"sketches":false}`. Every `error` and `warning` must be null. If something broke, restore or `undo` before you try anything else.
|
||||
3. **Interior features:** take one top-view screenshot, `{"source":"model","view":"top","width":480,"height":320}`. When the server is bridged, `view` also turns the user's camera. The iso view hides standoffs behind the walls.
|
||||
4. **Still parametric:** if the user wants it kept parametric, change one driving size, `measure`, then set it back.
|
||||
|
||||
## When something is already broken
|
||||
- **A feature has an `error` after opening:** read it with `timeline.inspect`, fix the input or definition, and recompute.
|
||||
- **The app crashed:**
|
||||
- `crash_reports` lists the reports, and `crash_reports {id}` gives the command that crashed.
|
||||
- `execute doc.recovery_list` and then `doc.recover {id}` reopen the autosaved design.
|
||||
- Never retry the crashing command unchanged.
|
||||
|
||||
## Save
|
||||
`save {"path":"...-v2.solidcraft"}` writes a copy. `save {}` overwrites the opened file; do that only when the user asked for it.
|
||||
|
||||
## Red flags
|
||||
|
||||
| Thought | Do instead |
|
||||
|---|---|
|
||||
| "timeline.inspect each feature to understand it" | `parameters.list {"filter":"feature"}` shows every input in one call. |
|
||||
| "Delete and remodel the feature" | Change its `dN` input, or use `timeline.edit`. History and downstream picks stay intact. |
|
||||
| "The insert worked, done" | Check the errors of every later feature. |
|
||||
| "Edge points with expressions, so the chamfer follows the size" | They are rejected, and stored picks already follow size changes. |
|
||||
| "Pick all 8 rim edges" | One pick on a tangent chain fillets or chamfers the whole chain. |
|
||||
| "Measure after every edit" | One batch, one `measure` against the summed prediction. Split only when it misses. |
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: solidcraft-modeling
|
||||
description: Use when building a new part, enclosure, bracket, plate, flange or any other 3D solid in SolidCraft through its MCP server (tools such as batch, execute, measure, screenshot), or when a SolidCraft session is using many tool calls or tokens.
|
||||
---
|
||||
|
||||
# SolidCraft modeling
|
||||
|
||||
## Overview
|
||||
Work the part out as numbers first. Then build it in a few large `batch` calls, and check each stage with `measure` against values you computed yourself. A clean build takes 3-4 build batches, about 10-13 calls with the checks. Command shapes, plane axes, ids, reply sizes and known traps are in `reference.md` next to this file, so you do not need to explore the command catalog.
|
||||
|
||||
## Before the first call
|
||||
1. Read `reference.md`.
|
||||
2. Run `inspect_design {"measure":false,"sketches":false}`, even when you think the session is empty.
|
||||
- The SolidCraft server's instructions end with `Backend: connected to the SolidCraft app` when it drives the user's open app, and with `Backend: headless` otherwise.
|
||||
- If the design has timeline features, ask before calling `new_design` or `open`, because both discard unsaved work. You can also build into the open design.
|
||||
3. Write a plan:
|
||||
- the parameters
|
||||
- the placement: centre the part on the origin so patterns and mirrors can work from the planes
|
||||
- every feature, with the world point you will use for each edge or face pick
|
||||
- the expected volume and bounding box
|
||||
|
||||
## Build
|
||||
- **Stages:** use one batch per stage:
|
||||
1. parameters, sketch, dimensions, finish and the first feature
|
||||
2. fillets and shell
|
||||
3. holes, cuts and patterns
|
||||
- **Failures:** a batch is all-or-nothing. On failure, read `failed.index` and `failed.error`, fix that step only, and send the batch again.
|
||||
- **Risky steps:** put a risky step in a batch of its own, so a failure does not roll back the work around it. This covers booleans between curved faces that touch, and anything you have not done before.
|
||||
- **Reply size:** in large designs, pass `"verbose": false` to `batch` and `execute`.
|
||||
- **Sketch ids are predictable,** so later steps in the same batch can use them:
|
||||
- Each sketch counts each kind from 1 (`l1`, `c1`, `a1`, `p1`).
|
||||
- Count what the earlier steps of that sketch created. For example, `rectangle.center` makes `l1`-`l6` and `p1`-`p5`.
|
||||
- Do not split a batch just to read ids back.
|
||||
- **Positions that must follow a parameter** come from:
|
||||
- sketch dimensions to `"origin"` with expression values (`"w/2 - wall - 6"`)
|
||||
- expression fields such as `solid.cylinder base:["w/2-6","d/2-6","wall"]`
|
||||
|
||||
A plain number never follows a parameter.
|
||||
- **Repeats:** `solid.pattern.rectangular` takes expression spacing; `solid.mirror` takes `XY`/`XZ`/`YZ`, a construction plane or `{origin, normal}`. A mirror or pattern feature cannot be mirrored or patterned again.
|
||||
- **Edge and face picks** snap to the nearest edge or face. There is no distance limit and no error, so compute the exact midpoint from your geometry (the vertical corner edge of a centred box is `[w/2, d/2, h/2]`).
|
||||
- Picks must be plain numbers.
|
||||
- A pick is resolved once and then follows its edge when parameters change.
|
||||
- **Profiles:** extrude takes every region by default. For a ring or a boss with a hole, pass `profiles`.
|
||||
- **Order:** fillet the outside edges before you shell, so the shell follows the fillets.
|
||||
|
||||
## Verify
|
||||
- **Each stage:** run `measure {}` (about 1 KB) and compare the volume, bbox and face count with your numbers. A cut or hole that leaves the volume unchanged has missed, even though it reported success.
|
||||
- **Timeline health:** `inspect_design {"measure":false,"sketches":false}` shows each feature's `error` and `warning`.
|
||||
- **Parameter test:** run `set_parameter`. Expect `errors: []`, then `measure` again; the COM and bbox should move by the amounts you computed.
|
||||
- **Positions:** `measure` does not say where a feature sits.
|
||||
- On an asymmetric part, check the COM shift you computed.
|
||||
- On a symmetric part (a mirrored or patterned layout) the COM does not move, so take a top-view screenshot and compare distances in pixels against a known size.
|
||||
- `body_topology` is a last resort: it costs 25-45 KB and splits cylinders into half faces.
|
||||
- **Final look:** one `screenshot {"source":"model","view":"iso","width":480,"height":320}`. Use `"view":"top"` for features inside walls, which the iso view hides.
|
||||
- `width` and `height` apply only to model renders.
|
||||
- When the server is bridged, `source` defaults to the whole app window and `view` also turns the user's camera, so pass `"source":"model"` and leave out `view` unless the user wants the camera moved.
|
||||
|
||||
## Finish
|
||||
`export {path}` writes `.step` for CAD or `.3mf`/`.stl` for printing. `save {path}` writes `.solidcraft`.
|
||||
|
||||
## Red flags
|
||||
|
||||
| Thought | Do instead |
|
||||
|---|---|
|
||||
| "List all commands first" | `reference.md`, or `list_commands {"filter":"<distinctive word>"}`. The unfiltered list is over 100 KB. |
|
||||
| "Check positions with body_topology" | `measure` COM and bbox against your numbers. |
|
||||
| "Inspect the sketch" | `sketch.solve` returns dof and the profile count in under 100 bytes. Run `sketch.inspect` on its own, never inside a big batch. |
|
||||
| "new_design for a clean start" | When the server is bridged, ask first. |
|
||||
| "It reported ok, so it worked" | Only a `measure` that matches the computed values counts. |
|
||||
@@ -0,0 +1,155 @@
|
||||
# SolidCraft command reference for agents
|
||||
|
||||
These are the shapes of the commands a part build needs, so an agent rarely has to call `list_commands`. The worked example at the end runs in CI (`skill_reference_example_builds_as_documented` in `crates/mcp/src/tests.rs`).
|
||||
|
||||
- Lengths are in mm.
|
||||
- Angles inside expressions take units: `"30 deg"`.
|
||||
- Any numeric field can be an expression that names parameters: `"w/2 - wall - 6"`.
|
||||
|
||||
## Sketch planes: where sketch (x, y) lands in the world
|
||||
|
||||
| `plane` | sketch x | sketch y | normal (+distance) |
|
||||
|---|---|---|---|
|
||||
| `"XY"` | +X | +Y | +Z |
|
||||
| `"XZ"` | +X | **-Z** | +Y |
|
||||
| `"YZ"` | +Y | +Z | +X |
|
||||
| `{origin, x_dir, y_dir}` | x_dir | y_dir | x_dir x y_dir |
|
||||
|
||||
- `offset: "expr"` on `sketch.create` moves the plane along its normal and follows parameters.
|
||||
- **Front wall (-Y) of a part centred on the origin:**
|
||||
- Sketch on `{"plane":"XZ","offset":"-d/2"}`.
|
||||
- Draw a feature at height z at sketch y = **-z**.
|
||||
- A cut with a positive distance goes into the wall.
|
||||
- **Top face:** sketch on `{"plane":"XY","offset":"h"}`.
|
||||
- **Hole direction:** `solid.hole {sketch}` drills from the sketch plane along -normal. A hole sketch at z=0 under a part drills into empty space and still reports success, so put the sketch on the top face.
|
||||
|
||||
## Ids
|
||||
|
||||
- **Numbering:** every creator returns `created: {points, curves, constraints, dimensions}`. Ids are stable and numbered per sketch.
|
||||
- **Curves:** `l1`, `c1`, `a1`.
|
||||
- **Points:** `p1`, `l1.start`, `l1.end`, `c1.center`, `a1.center`, and the sketch origin `"origin"`.
|
||||
- **Rectangles:** `sketch.rectangle.*` also returns `sides {bottom,right,top,left}` and `corners {bottom_left,...}`.
|
||||
- `rectangle.center` adds 2 construction diagonals, so each corner joins 3 lines.
|
||||
- `sketch.fillet {point}` therefore fails on its corners. Use `{a:"l1", b:"l4", radius}` or the 3D `solid.fillet`.
|
||||
- **Slots:** `sketch.slot.overall` and `sketch.slot.center_to_center` create curves `l1, a1, l2, a2` and points `p1`-`p4`, `a1.center`, `a2.center`.
|
||||
- The `length` role of `slot.overall` is the overall length. The arc centres are at ±(length/2 - width/2).
|
||||
- After `length` and `width`, 3 dof remain. To remove them, dimension `origin` to `a1.center` horizontally and vertically, and `origin` to `a2.center` vertically.
|
||||
- `sketch.slot.center_point` creates no centre point. Dimension an arc centre instead.
|
||||
- **Circles:** `sketch.circle.center` creates `c1` and `c1.center`, and no `p` point.
|
||||
- **Dimensions** share the `k` counter with constraints.
|
||||
- **Constraints can add curves:** a constraint about an axis, such as `sketch.constraint.symmetry` with `line:"y"`, adds a construction line and so takes the next `l` id. Read `created` before you count further.
|
||||
- **Over-constraining** is refused with `that conflicts with the sketch: neither entity can move to satisfy it`. For example, symmetry already makes two circles equal.
|
||||
|
||||
## Dimensions
|
||||
|
||||
```json
|
||||
{"command":"sketch.dimension","params":{"entities":["origin","c1.center"],"type":"horizontal","value":"w/2 - wall - 6"}}
|
||||
{"command":"sketch.dimension","params":{"entities":["c1"],"type":"diameter","value":"6"}}
|
||||
{"command":"sketch.dimension","params":{"entities":["l1"],"value":"w"}}
|
||||
```
|
||||
|
||||
Distances are unsigned. Draw the geometry on the correct side first, and the dimension keeps it there.
|
||||
|
||||
`sketch.draw.dimensioned` creates a shape and its dimensions in one undo step. The dimension roles depend on the creator:
|
||||
|
||||
| Creator | Roles |
|
||||
|---|---|
|
||||
| `line` (exactly 2 points) | `length`, `angle` |
|
||||
| `rectangle.*` | `width`, `height` |
|
||||
| `circle.*` | `radius` or `diameter` |
|
||||
| `arc.center_point` | `radius`, `sweep` |
|
||||
| `slot.center_to_center`, `slot.overall` | `length`, `width` |
|
||||
| `polygon.*` | `radius` |
|
||||
|
||||
```json
|
||||
{"command":"sketch.draw.dimensioned","params":{"command":"sketch.rectangle.center","params":{"center":[0,0],"corner":[35,25]},"dimensions":{"width":"w","height":"d"}}}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
| Command | Minimal params | Traps |
|
||||
|---|---|---|
|
||||
| `solid.extrude` | `distance` | `profiles` defaults to **every region**, so two concentric circles give a solid disc. Pick regions with `[{"point":[x,y]}]` or `[index]`. Other options: `operation` (new/join/cut/intersect/auto), `direction` (positive/negative/symmetric), `through_all: true`, `start_offset`. A cut that misses reports success. |
|
||||
| `solid.box` | `length`, `width`, `height` | `center:[x,y,z]` or `corner`. The quickest way to block out a part. |
|
||||
| `solid.cylinder` | `radius` or `diameter`, `height` | `base` is the centre of the bottom (default origin), `axis` defaults to `Z`. Expressions work in `base`. Always pass `operation`. |
|
||||
| `sketch.slot.overall` / `sketch.slot.center_to_center` | `p0`, `p1` (overall ends / arc centres), `width` | Ids and dof are in the "Ids" section above. |
|
||||
| `solid.fillet` / `solid.chamfer` | `edges:[[x,y,z]]`, `radius` / `distance` | The pick snaps to the **nearest** edge, with no distance limit and no error, so check that the face count changed. One pick takes the whole tangent chain: a rounded rim needs 1 point, not 8. |
|
||||
| `solid.shell` | `faces:[[x,y,z]]`, `thickness` | A point on each face to remove. Fillet first. |
|
||||
| `solid.hole` | `position:[x,y,z]` on a face + `diameter`, or `sketch` (+ `points`) | `clearance:"M3"`. `depth` defaults to through all. `type` is one of simple/drilled/counterbore/countersink. A `countersink` needs `cs_diameter` and `cs_angle`; a `counterbore` needs `cb_diameter` and `cb_depth`. `position` is a fixed point. |
|
||||
| `solid.pattern.rectangular` | `features`, `dir1`, `count1`, `spacing1` (+ `dir2`, `count2`, `spacing2`) | Spacing takes expressions and follows parameters. |
|
||||
| `solid.pattern.circular` | `features`, `axis` (`"Z"` or `{origin, dir}`), `count` | `angle` defaults to 360 deg. A full pattern keeps the COM on the axis, so check its radius with a top-view screenshot. |
|
||||
| `solid.mirror` | `features:[names]`, `plane` (`XY`/`XZ`/`YZ`, a construction plane, or `{origin, normal}`) | Follows parameters. Mirror and pattern features cannot be mirrored or patterned again. |
|
||||
|
||||
- **Names:** name every feature (`"name":"Standoff"`) and keep names unique across sketches and features. `timeline.roll_to {"feature":"Plate"}` picks the first match, which can be the sketch.
|
||||
- **Counts as parameters:** `parameters.add {"name":"n","expression":"6","unit":""}` creates a unitless parameter, and `"count":"n"` uses it in a pattern.
|
||||
- **Holes on a parametric bolt circle:**
|
||||
1. Cut one hole with `solid.cylinder {"diameter":6.6,"height":"T + 2","base":["bcd/2",0,-1],"operation":"cut","name":"BoltHole"}`.
|
||||
2. Repeat it with `solid.pattern.circular {"features":["BoltHole"],"axis":"Z","count":"n"}`.
|
||||
|
||||
Both the radius and the count follow their parameters.
|
||||
- **Several parameter changes:** use one `batch` of `parameters.change` steps instead of one `set_parameter` call each.
|
||||
- **Primitive booleans:** a `solid.box` or `solid.cylinder` cut that touches curved faces of the target can fail with `the exact fallback ... takes planar faces only`. Use a sketch profile and an extrude cut instead, for example a slot sketch rather than a box plus 2 cylinders.
|
||||
|
||||
## Worked example: parametric plate with 4 counterbored holes (1 batch)
|
||||
|
||||
```json
|
||||
{"commands":[
|
||||
{"command":"parameters.add","params":{"name":"L","expression":"80 mm"}},
|
||||
{"command":"parameters.add","params":{"name":"W","expression":"50 mm"}},
|
||||
{"command":"parameters.add","params":{"name":"T","expression":"6 mm"}},
|
||||
{"command":"parameters.add","params":{"name":"inset","expression":"8 mm"}},
|
||||
{"command":"sketch.create","params":{"plane":"XY","name":"PlateSketch"}},
|
||||
{"command":"sketch.draw.dimensioned","params":{"command":"sketch.rectangle.center","params":{"center":[0,0],"corner":[40,25]},"dimensions":{"width":"L","height":"W"}}},
|
||||
{"command":"sketch.finish","params":{}},
|
||||
{"command":"solid.extrude","params":{"distance":"T","name":"Plate"}},
|
||||
{"command":"solid.fillet","params":{"edges":[[40,25,3],[-40,25,3],[40,-25,3],[-40,-25,3]],"radius":5,"name":"Corners"}},
|
||||
{"command":"sketch.create","params":{"plane":"XY","name":"HolePts","offset":"T"}},
|
||||
{"command":"sketch.point","params":{"point":[32,17]}},
|
||||
{"command":"sketch.dimension","params":{"entities":["origin","p1"],"type":"horizontal","value":"L/2 - inset"}},
|
||||
{"command":"sketch.dimension","params":{"entities":["origin","p1"],"type":"vertical","value":"W/2 - inset"}},
|
||||
{"command":"sketch.finish","params":{}},
|
||||
{"command":"solid.hole","params":{"sketch":"HolePts","diameter":3.4,"type":"counterbore","cb_diameter":6.5,"cb_depth":3.3,"name":"Hole"}},
|
||||
{"command":"solid.pattern.rectangular","params":{"features":["Hole"],"dir1":[-1,0,0],"count1":2,"spacing1":"L - 2*inset","dir2":[0,-1,0],"count2":2,"spacing2":"W - 2*inset","name":"Holes"}}
|
||||
]}
|
||||
```
|
||||
|
||||
**Expected volume:**
|
||||
- Plate: `L*W*T - 4*r²(1-π/4)*T`
|
||||
- Each hole: `π*1.7²*(T-3.3) + π*3.25²*3.3`
|
||||
- Total: 23335.2 mm³, with 22 faces.
|
||||
|
||||
After `set_parameter L=120` the volume is 35335.2 mm³, and the holes stay in the corners.
|
||||
|
||||
## Reading `measure`
|
||||
|
||||
- **Tolerance:** analytic and measured volumes agree to about 0.001-0.01%, so treat a gap that small as a match.
|
||||
- **Face counts:** compare the logical `faces`, not the larger `kernel.faces`.
|
||||
- **Reply types:** `set_parameter` returns `recomputed` as a number. A batch reply echoes every step.
|
||||
- **Boolean warning:** a feature `warning` such as `Boolean retry: temporary coincident-face separation up to 2.050000e0 mm per face` means the kernel nudged faces that coincide. Examples are a hole floor on another floor, or a join flush on a face. When the volume still matches your number the warning is harmless.
|
||||
|
||||
## Reply cost (0.8, plate example)
|
||||
|
||||
| Call | Size |
|
||||
|---|---|
|
||||
| `list_commands {}` | **Over 100 KB.** Never call it unfiltered; filter on a distinctive word such as `"hole"` or `"slot"`. |
|
||||
| `body_topology` | 25-45 KB |
|
||||
| `inspect_design {}` | 12 KB, and it grows with the sketches |
|
||||
| `inspect_design {"measure":false,"sketches":false}` | 4 KB: parameters, timeline errors, bodies |
|
||||
| `sketch.inspect` | 5 KB per small sketch |
|
||||
| `parameters.list {"filter":"feature"}` | 3 KB |
|
||||
| `measure {}` | 1 KB: volume, bbox, COM, face/edge/vertex counts |
|
||||
| `sketch.solve` | under 100 B: dof, status, profile count |
|
||||
| `screenshot {"source":"model","width":480,"height":320}` | one small image. Without `source`, a bridged server returns the whole app window and ignores the size. |
|
||||
|
||||
## Errors
|
||||
|
||||
| Error text | Fix |
|
||||
|---|---|
|
||||
| `... is not available right now: no sketch is being edited` | Run `sketch.create` or `sketch.edit {sketch}` first. |
|
||||
| `unknown sketch entity 'p1'` | Use the ids from `created`. A circle's centre is `c1.center`. |
|
||||
| `the corner must join exactly two lines` | It is a `rectangle.center` corner. Use the `{a, b}` form. |
|
||||
| `` `cs_diameter` must be a number or an expression `` | Countersinks need `cs_diameter` and `cs_angle`, even with `clearance`. |
|
||||
| `unsupported dimension role for this creator` | Check the roles table above. |
|
||||
| `<Feature> cannot be patterned or mirrored yet` | Pattern the original feature, for example one 2x2 rectangular pattern. |
|
||||
| `an edge is a point [x,y,z], {point} or {body, index}` | An edge pick contained an expression. Use plain numbers. |
|
||||
| `boolean Cut: the operation failed ... planar faces only` | See "Primitive booleans" above. |
|
||||
Reference in new issue
Block a user