assembly-solver
Skill140 starsChanged 5 months agoApache-2.0View on GitHub
This skill should be used when working on brepjs assemblies, constraint solving, or kinematics — debugging a solve that returns ASSEMBLY_NOT_CONVERGED / ASSEMBLY_MATE_INVALID / ASSEMBLY_SOLVE_FAILED, adding or extending a mate or solver entity-pair, adding a joint type, or driving a mechanism. Trigger phrases include "assembly won't converge", "Unsupported constraint types", "solveAssembly returns Err", "add a mate/joint/constraint type", "extend TRANSLATIONAL_PAIRS", "solveMate: unsupported entity pair escaped filter", "revolute/prismatic/cylindrical/planar/spherical joint", "forwardKinematics", "inverse kinematics / IK target", "DH table / Denavit-Hartenberg", "export/import URDF", or "positioned STEP export from an assembly". Not for boolean fuse/cut compounds (that is debugging-geometry).
In this repository18 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- llms-full.txtllms.txt
- llms.txtllms.txt
- adding-operationsSkill
- architecture-navigationSkill
- ci-triageSkill
- companion-packagesSkill
- debugging-geometrySkill
- git-pr-workflowSkill
- kernel-abstractionSkill
- memory-and-disposalSkill
- playground-examplesSkill
- quality-gatesSkill
- release-publishingSkill
- result-error-handlingSkill
- shaperef-lineageSkill
- wasm-interopSkill
- writing-testsSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 21
Read CLAUDE.md's "Assembly solver composes constraints down a chain" gotcha first — it is the authoritative one-liner.
- Distinctive: line 23
Every node starts at origin/identity.
- Very specific: line 39
If extraction returns null for either entity, solveAssembly fails with ASSEMBLY_MATE_INVALID ("could not extract geometry from mate …
- Very specific: line 78
Add the key to TRANSLATIONAL_PAIRS.
- Very specific: line 79
Add the matching case in the solveTranslational switch (with a per-pair solve function like the existing solvePlanePair, solveConcentric, …
- Very specific: line 97
Add a joint type: write a constructor building dofs via makeDof + buildJoint.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Assemblies, constraint solving, and kinematic joints
Covers two Layer-2 subsystems: the immutable assembly tree with its analytical constraint solver, and drivable kinematic joints (forward/inverse kinematics, DH, URDF). All of it is pure data + math with no kernel calls, except the STEP export at the very end.
Two unrelated meanings of "assembly" — do not conflate
This is the number-one source of confusion. There are two "assembly" APIs that share a word and nothing else.
- Assembly tree + mates + joints — an immutable
AssemblyNodetree (src/operations/assemblyFns.ts), constraint mates (src/operations/mateFns.ts), and kinematic joints (src/operations/jointFns.ts). Pure data + math. This is what the constraint solver and all the kinematics operate on. - Assembly STEP export —
exportAssemblySTEP/createAssembly/exportSTEP(src/operations/exporterFns.ts,src/operations/exporters.ts). An XCAF colored/named multi-shape STEP writer. It takes a flatShapeOptions[], not anAssemblyNode, and never reads mates, joints, or solved transforms.
There is no bridge between them. A solved tree's transforms are not fed into exportAssemblySTEP. To write a positioned STEP, apply the solved transforms to the shapes and pass them as ShapeOptions[].
The solver mental model
Read CLAUDE.md's "Assembly solver composes constraints down a chain" gotcha first — it is the authoritative one-liner. solveConstraints(nodes, constraints) in src/kernel/solverAdapter.ts is the analytical (closed-form, non-iterative) core. Its algorithm:
- Every node starts at origin/identity.
- A positioning mate is one of
coincident,distance,angle,concentric(thePOSITIONING_TYPESset) with both entities present.entityAis the reference,entityBis the dependent. - Anchors sit at the origin: any node that is never a dependent (a chain root), plus any explicit
fixednode. - Well-typed mates resolve in topological rounds: a mate places its dependent once its reference is already placed, solving against the reference's solved world-space pose (rotation included), so multi-body chains compose.
- Diagnostics: an entity-type mismatch is pushed as
type(a-b)and dropped; a mate whose reference never resolves (cycle/dangling) is pushed astype(unanchored).converged = unsupported.length === 0.
mateFns.solveAssembly wraps this: it extracts geometry, calls solveConstraints, and maps the result to a Result<AssemblySolveResult>.
From MateEntity to a solver entity type
A MateEntity is { node, face?, edge?, point? }. extractEntity in mateFns.ts maps it to a solver entity typed plane, axis, or point:
- face →
faceAxis(face)(fromfaceFns.ts); non-null (a cylindrical/axial face) →axis, elseplaneviafaceCenter+normalAt.faceAxisreturning null is the axial-vs-planar discriminator. - edge →
LINEtype →axisalong its tangent; elsecurveAxis(circular edge, e.g. a bore rim) →axis; elsenull. - point →
point.
If extraction returns null for either entity, solveAssembly fails with ASSEMBLY_MATE_INVALID ("could not extract geometry from mate entities").
Supported constraint pairs
isSupportedPair (in solverAdapter.ts) decides solvability:
| Mate type | Allowed (entityA-entityB) pairs |
coincident, distance | TRANSLATIONAL_PAIRS: plane-plane, plane-point, point-plane, point-point, axis-axis, axis-point, point-axis |
concentric | REQUIRED_ENTITIES: axis-axis only |
angle | REQUIRED_ENTITIES: plane-plane only |
fixed | one entity, anchors that node at origin |
Both orders are listed for translational pairs, so entities need not be pre-ordered.
Debugging a failed solve
The three error codes live in src/core/errors.ts (ASSEMBLY_MATE_INVALID, ASSEMBLY_SOLVE_FAILED, ASSEMBLY_NOT_CONVERGED). URDF errors instead use the generic VALIDATION_FAILED; STEP export uses STEP_EXPORT_FAILED.
Symptom → cause → fix
| Symptom | Cause | Fix |
ASSEMBLY_MATE_INVALID: no mates defined | Called solveAssembly before addMate | Add at least one mate |
ASSEMBLY_MATE_INVALID: could not extract geometry | A MateEntity face/edge yielded no plane/axis/point (e.g. a non-line, non-circular edge) | Pick an axial/planar face, a straight or circular edge, or a point |
ASSEMBLY_NOT_CONVERGED: Unsupported constraint types: coincident(axis-plane) … | Entity-type mismatch — pair not in the supported table (the canonical case is coincident(axis-plane)) | Fix the entities so the pair is supported (e.g. axis-axis for concentric, plane-plane for angle) |
ASSEMBLY_NOT_CONVERGED: … concentric(plane-plane) … | concentric/angle got the wrong entity types (they require axis-axis / plane-plane) | Feed the required entity types |
ASSEMBLY_NOT_CONVERGED: … coincident(unanchored) … | Reference never resolved — a mutual-reference cycle (a→b, b→a) or a dangling reference, with no root/fixed anchor | Add a fixed mate to anchor the chain, or break the loop |
ASSEMBLY_NOT_CONVERGED: … solver did not converge (no unsupported list) | Rare: no diagnostics but still not converged | Inspect the mate graph for an unanchored dependent |
ASSEMBLY_SOLVE_FAILED: … | An exception was thrown inside the solve (see below) | Read the wrapped message; likely the solveMate escaped-filter invariant |
The dof in the message is the sum over unsupported mates of UNSUPPORTED_DOF (coincident 3, concentric 4, distance 1, angle 1). tests/mateFns.test.ts has worked examples of every diagnostic, including the axis-plane and the a→b/b→a cycle.
Extending the solver (new mate or entity pair)
The load-bearing invariant: solveMate's default branch throws "solveMate: unsupported entity pair escaped filter" when solveTranslational returns null after isSupportedPair passed — i.e. the pair set and the dispatch switch drifted out of sync (surfaces as ASSEMBLY_SOLVE_FAILED).
To add a translational entity pair (e.g. a new axis-plane handling), update both in solverAdapter.ts:
- Add the key to
TRANSLATIONAL_PAIRS. - Add the matching
casein thesolveTranslationalswitch (with a per-pair solve function like the existingsolvePlanePair,solveConcentric,solveAxisToPoint).
For a new orientation/axis mate, add to REQUIRED_ENTITIES and give it a branch in solveMate; add its type to POSITIONING_TYPES and UNSUPPORTED_DOF. Then surface it through mateFns.ts: extend the MateConstraint union and mateToSolverConstraint, and export from the public surface. See the adding-operations skill for the export/function-lookup.md gate and result-error-handling for adding an error code.
Joints and kinematics
src/operations/jointFns.ts defines five joint types. A Joint connects parent (stationary reference) → child (moving); dofs is the source of truth; value/min/max mirror the primary (first) DOF for single-DOF ergonomics, and axis is the joint's anchor/primary axis (equal to the primary DOF axis only for single-axis joints, not for spherical/planar). Every DOF value is always clamped to [min, max] (makeDof normalizes inverted ranges).
| Joint | DOFs | Default ranges |
revolute | 1 rotation | -180..180 |
prismatic | 1 translation (ignores axis.origin) | 0..100 |
cylindrical | rotation + slide on one axis | rot -180..180, trans 0..100 |
planar | u-trans, v-trans, rotation about normal | u/v -100..100, rot -180..180 |
spherical | x, y, z rotations about a pivot (Rx·Ry·Rz) | each -180..180 |
- Drive:
setJointValues(joint, number[])sets per-DOF positionally;setJointValue(joint, n)sets only the primary.jointTransform(joint, value?)returns the child's local pose — a singlenumberoverrides the primary DOF only, an array overrides positionally. - Forward kinematics:
forwardKinematics(assembly, jointValues?)returns a worldJointPosefor every node, keyed by child name, resolved topologically:childWorld = parentWorld ∘ jointTransform ∘ offset?. Roots sit at origin.mechanismDOFsums open-chain DOFs (closed-loop Grübler/Kutzbach is future work). - Add a joint type: write a constructor building
dofsviamakeDof+buildJoint. FK/IK differentiate throughdofs, so kinematics stays joint-agnostic automatically.
Driving: inverse kinematics and trajectories
src/operations/ikFns.ts. inverseKinematics(assembly, endEffector, target, options?) is damped-least-squares over a numerically-differentiated Jacobian of forwardKinematics — joint-type agnostic. IKTarget is { position, rotation? }; omit rotation for position-only (m=3 vs m=6). Options: maxIterations (200), tolerance (1e-5), damping (0.05), seed, tip. Gotcha: the finite-difference Jacobian steps inward from a bound, because forwardKinematics clamps each DOF — a forward +eps at a limit would give a zero column and trap the solver.
jointTrajectory(assembly, from, to, steps) samples a straight line in joint space, returning steps + 1 samples (both endpoints inclusive). Joints absent from from/to hold their stored value.
DH and URDF interchange
src/operations/dhFns.ts — jointsFromDH(rows, {base?}) builds a serial revolute/prismatic chain from a distal DH table. Each row → one joint; the variable is θ (revolute) or d (prismatic) about/along +z, and the fixed link geometry Rz(θ)·Tz(d)·Tx(a)·Rx(α) rides on Joint.offset so each row contributes exactly one DOF.
src/operations/urdfFns.ts — exportURDF round-trips only revolute/prismatic; it errors (VALIDATION_FAILED) on any multi-DOF joint or any joint carrying an offset (so DH chains cannot exportURDF). Revolute limits convert degrees↔radians on the boundary; prismatic emits a zero origin (brepjs prismatic FK ignores axis.origin). importURDF(xml) is a regex reader: continuous → revolute -180..180, and fixed/floating/planar joints are skipped (their links still listed).
Assembly STEP / XCAF export
exportAssemblySTEP(shapes: ShapeOptions[], { unit?, modelUnit? }) → Result<Blob> (MIME application/STEP). ShapeOptions is { shape, color?, alpha?, name? }. The OOP twin createAssembly returns a disposable AssemblyExporter (a KernelHandle); exportSTEP disposes it via Symbol.dispose. See src/operations/README.md for units/colors — do not restate them. This path needs the kernel's createXCAFDocument / writeXCAFToSTEP; assembly-STEP XCAF is a known occt-wasm divergence, so check the kernel-abstraction skill for current support. For handle disposal see memory-and-disposal.
Where things are exported (asymmetry to know)
Everything is in src/operations.ts and re-exported from root brepjs — except mates: addMate, solveAssembly, MateConstraint, MateEntity, AssemblySolveResult are exported only from root brepjs (src/index.ts), not from brepjs/operations. The solver core (solveConstraints, SolverEntity, SolverConstraint) is not barrelled at all — import it directly from @/kernel/solverAdapter.js, as the tests do.
Additional resources
tests/mateFns.test.ts— every solve + diagnostic case;tests/jointFns.test.ts,tests/dhFns.test.ts,tests/ikFns.test.ts,tests/urdfFns.test.ts,tests/assemblyFns.test.ts— usage examples.src/operations/README.md— the STEP/XCAF export details (units, colors, disposal).
Sibling skills
result-error-handling— unwrappingResult, adding aBrepErrorCode.kernel-abstraction— whethercreateXCAFDocument/writeXCAFToSTEPare supported per kernel.adding-operations— the export surfaces +function-lookup.mdgate when adding a mate/joint function.writing-tests— the test skeleton.memory-and-disposal— theAssemblyExporterKernelHandleandusing.debugging-geometry— for fused-compound "assemblies", which are a different thing entirely.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

