fluid-cad/fluidcad/fluidcad-assembly
Claude Code and agent skill by fluid-cad/fluidcad
- community
Checking that the SKILL.md file is still in the repository.
Building multi-part models in FluidCAD: assemblies, part files, inserting and mating parts, connectors, exposed geometry, part parameters, sub-assemblies, replicated instances, STEP parts, project units, and assembly export. Use this skill whenever the user wants an assembly, a mechanism, a hinge, a slider, several parts that fit or move together, a `.assembly.js` or `.part.js` file, `insert()`, `mate()`, `connector()`, `expose()`, `replicate()`, instances, or asks to split a model into parts. Trigger it alongside the FluidCAD skill any time the FluidCAD MCP tools (`mcp__FluidCAD__*`) are available and more than one part is involved.
Install
npx vskill@latest install fluid-cad/fluidcad/fluidcad-assemblySKILL.md
Provenance: maintained in the FluidCAD repo (https://github.com/Fluid-CAD/FluidCAD), under mcp/skills/. The installed local skill files are the runtime source of truth.
Assemblies in FluidCAD
This skill layers on top of the fluidcad skill. That one owns the loop: read the docs, plan in generic CAD terms, build in increments, check render.state and objectErrors, resolve filters before writing them, screenshot when a feature earns it, roll back to debug, and report only what was checked. Follow it for every part. This skill covers only what is specific to models made of several parts: when to split, how a part publishes its interface, how an assembly places and joins instances, and how to verify the result.
The governing principle: a part is verified alone before it is inserted, and mates are added one at a time. An assembly is where three parts' mistakes meet; keep the mistakes out of it.
1. When to split a model into parts
Split when the pieces are separate physical components: things fabricated separately, bought (a bolt, a bearing, a rail), moving relative to each other (a lever on a pin, a carriage on a rail), or reused with different parameters (four standoffs of two heights). Features of one machined or printed body (holes, ribs, fillets) are never parts.
Two rules from the API decide the mechanics:
- Inside a
part(), shapes auto-fuse with each other and never with anything outside it. Two solids that touch but must stay separate go in two parts. Inside one part,.new()keeps a feature as a separate body. - Every
part()is exported (export const x = part("X", () => { … })at the top level of its file, never inside anotherpart()orassembly()body) and its name is unique within the file; a part breaking either is a build error (part-ruleson a write). Only an exported part can be inserted. A standalone model needs nopart()at all.
Convention: one part per .part.js file, named after the thing it is (bracket.part.js exports bracket); one .assembly.js file per mechanism. Several components in one file each get their own part(), but that file cannot be inserted piecemeal with the same clarity, so prefer one file each.
2. Files and units
| suffix | scene | contents | |
|---|---|---|---|
.part.js | part | modelling statements, or export const x = part("X", () => { … }) (a part is always exported) | |
.assembly.js | assembly | export const x = assembly("x", () => { insert(); mate(); … }) | |
.fluid.js | part | legacy suffix, same as .part.js | |
.params.js | never rendered | export const wall = param("Wall thickness", 3) — project parameters every part and assembly imports |
Units belong to parts:
- A part's numbers are in its file's unit:
unit('in')directly after the imports, else the"unit"in the nearestfluidcad.json, elsemm. - An assembly never calls
unit()(the write guard refuses it, codeunit-statement). Every length the assembly file owns,.translate(), mate.offset()and.limits(), is in the project unit. - Inserted parts are scaled from their file's unit into the project unit automatically, connectors included. An inch part in a mm project comes out at 25.4 times its inch numbers with nothing to write.
insert(def, { Length: 10 })overrides are read in the part's unit, whatever the assembly's. Theinch()/mm()helpers do not help here; they convert into the calling file's unit, which is the assembly's.
Before trusting any number, read unit from get_scene_summary for the file that is open.
3. Part-local origin conventions
The part's origin is its mating interface. A dimension measured from that interface then appears in the code verbatim, and the connector at the origin needs no offset.
- A plate or base: bottom (mounting) face on
z = 0, outline centered on the origin when the part is symmetric, otherwise a datum corner at the origin. - A pin, shaft, standoff, bolt: axis on Z, the seating face (head underside, foot) on
z = 0. - A lever, link, bracket: the pivot bore axis on Z through the origin; the arm extends along +X.
- A carriage or slider: the sliding face on
z = 0, travel along X. - A symmetric part: the symmetry plane through the origin, so mirrored features are free and the connector on the symmetry plane is at
x = 0.
State the convention in one line before writing the part, exactly as the core skill asks for a single part. The assembly's starting poses (.translate()) become readable when every part's origin is where it mates.
Every part gets its own colour, written as the last statement of its body (color("steelblue") with no selection paints the whole part; a multi-body part such as a bearing colours each body: color("silver", outer), color("dimgray", balls)). Instances inherit it, so the assembly screenshot names the parts by colour and a section shows where one part ends and the next begins. Grey-on-grey is how an overlap between a screw and a bearing goes unnoticed. Palette and rules in the core skill ("Colour every body that meets another body"): housings and big parts cool and light, fasteners, pins and balls warm, no two mating parts alike.
4. A part's interface: param(), connector(), expose()
| statement | direction | read back as | |
|---|---|---|---|
param("Label", default, type?, opts?) | values in | insert(def, { Label: value }) | |
connector("name", geometry) | mate frames out | instance.connectors.name for mate() | |
expose("name", sceneObject) | geometry out | def.features.name (another part builds on it), instance.features.name (a tangent mate touches it) |
Rules that trip up every first attempt:
param()is only valid inside apart()orassembly()body, or exported from the top level of a*.params.jsfile; at the top level of a model file it throws. Declare parameters at the top of the body, before the geometry that uses them.- A dimension two parts must agree on is a project parameter, not an override. The bore of the housing and the shaft diameter, the wall thickness of both halves, the bolt size of a flange and its mate:
export const x = param("Label", value)inproject.params.js, thenimport { x } from "./project.params.js"in each part. Never copy the number into the second file, and never push it throughinsert(def, { Label })— an override naming a project parameter is warned about, not applied.list_global_params/ theglobalParamsblock ofget_scene_summarytell what exists; a*.params.jswrite re-renders the open model (the file itself is never the model). - A named value set is a `configurations()` row, not a copy. "A compact and a wide version", "M6 and M8 variants":
configurations({ Compact: { Width: 500 }, Wide: { Width: 1200 } })as the first statement of the body, before itsparam()calls; a row lists only the labels it changes. Insert a row by name,insert(def, 'Wide')(orinsert(def, 'Wide', { Height: 20 })to override on top), never by copying its numbers intoinsert(def, { Width: 1200 }). To show a row of the open model useselect_configuration;export,screenshotandpack_modeltakeconfigurationfor one call without switching.get_scene_summary().configurationslists the rows. connector()andexpose()are declared directly in the part body, not in a nested callback; names are unique within a part.- A connector's source must resolve to exactly one face, edge or vertex, or be a plane; a raw point is refused inside a part.
resolve_selectionthe expression at the part's scope and confirmcountis 1 before writing it. - A face connector sits at the face center with Z along the outward normal; a circular edge gives center plus axis; a straight edge gives midpoint plus tangent.
.offset(x, y, z)and.rotate(axis, deg)move it in its own axes, in call order. - Face frames point Z out of the solid, and a mate places the second Z against the first by default, so connectors on the touching faces of two parts put the parts on each other with no options.
- A sketch a feature used is only hidden, never consumed, so it can still be passed to
expose(). - A hole pattern gets one connector and a `copy()`, not one connector per hole. Declare it on the first hole, then copy it in the same part body:
copy(holes, bolt)follows therepeat()that made the holes, so every copy stays on its hole when the repeat's count, spacing or axis changes (partial arcs included);copy("linear" | "circular", axis, options, bolt)patterns it on its own.repeat()refuses connectors. - Copy k of
boltisbolt.instance(k)in the part file andinstance.connectors.bolt.instance(k)in an assembly, a mate side and replicate cell like any connector. Slots number likerepeat().instance(k): a linear grid with the first axis varying slowest and the original in its own cell, a circular copy by steps from 0, the follow form by the repeat's own slots.bolt.instance(<original's slot>)isbolt. - A copy is the seed's frame moved rigidly, never re-derived from geometry: a linear or circular copy sits on a hole only while its pattern matches the holes', and a circular copy steps
angle / countwhererepeat()steps a partial arcangle / (count - 1). Follow the repeat when the connector belongs on repeated geometry. - Copies are read-only (
.offset()/.rotate()on one throws; edit the seed or the copy statement). One copy statement per connector, no copy of a copy (a grid is one two-axis linear copy), explicit targets only, and nocenteredon a circular copy of a connector. Changing a later grid axis's count, or acenteredcount, renumbers the slots and moves whatever is mated to them. Check the copies withget_scene_summary: each is aconnectorobject namedbolt.instance(k)carrying its frame.
A standoff with its mating interface at the origin:
On the plate side, one connector is the top-face frame moved along its own X and Y to the first hole's center (the offsets read like the hole sketch coordinates), and a linear copy with the holes' pitch puts a copy on each of the others:
When the holes come from a repeat() (const holes = repeat(...) of the hole's cut()), anchor the connector on the hole itself and follow the repeat instead: const bolt = connector("bolt", hole.startEdges()); copy(holes, bolt);.
5. STEP parts via load()
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingmcp/skills/fluidcad-assembly/SKILL.md
Popularity
- Repository stars
- 662
- Repository forks
- 40
- 7-day trend score
- 32
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/fluid-cad/fluidcad/fluidcad-assembly)