agentleFS
andymaiandymai/brepjs/.claude/skills/assembly-solver/SKILL.md

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).

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
  1. Very specific:

    Read CLAUDE.md's "Assembly solver composes constraints down a chain" gotcha first — it is the authoritative one-liner.

    line 21
  2. Distinctive:

    Every node starts at origin/identity.

    line 23
  3. Very specific:

    If extraction returns null for either entity, solveAssembly fails with ASSEMBLY_MATE_INVALID ("could not extract geometry from mate …

    line 39
  4. Very specific:

    Add the key to TRANSLATIONAL_PAIRS.

    line 78
  5. Very specific:

    Add the matching case in the solveTranslational switch (with a per-pair solve function like the existing solvePlanePair, solveConcentric, …

    line 79
  6. Very specific:

    Add a joint type: write a constructor building dofs via makeDof + buildJoint.

    line 97

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

  1. Assembly tree + mates + joints — an immutable AssemblyNode tree (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.
  2. 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 flat ShapeOptions[], not an AssemblyNode, 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 (the POSITIONING_TYPES set) with both entities present. entityA is the reference, entityB is the dependent.
  • Anchors sit at the origin: any node that is never a dependent (a chain root), plus any explicit fixed node.
  • 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 as type(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) (from faceFns.ts); non-null (a cylindrical/axial face) → axis, else plane via faceCenter + normalAt. faceAxis returning null is the axial-vs-planar discriminator.
  • edge → LINE type → axis along its tangent; else curveAxis (circular edge, e.g. a bore rim) → axis; else null.
  • 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 typeAllowed (entityA-entityB) pairs
coincident, distanceTRANSLATIONAL_PAIRS: plane-plane, plane-point, point-plane, point-point, axis-axis, axis-point, point-axis
concentricREQUIRED_ENTITIES: axis-axis only
angleREQUIRED_ENTITIES: plane-plane only
fixedone 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

SymptomCauseFix
ASSEMBLY_MATE_INVALID: no mates definedCalled solveAssembly before addMateAdd at least one mate
ASSEMBLY_MATE_INVALID: could not extract geometryA 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 anchorAdd 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 convergedInspect 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:

  1. Add the key to TRANSLATIONAL_PAIRS.
  2. Add the matching case in the solveTranslational switch (with a per-pair solve function like the existing solvePlanePair, 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).

JointDOFsDefault ranges
revolute1 rotation-180..180
prismatic1 translation (ignores axis.origin)0..100
cylindricalrotation + slide on one axisrot -180..180, trans 0..100
planaru-trans, v-trans, rotation about normalu/v -100..100, rot -180..180
sphericalx, 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 single number overrides the primary DOF only, an array overrides positionally.
  • Forward kinematics: forwardKinematics(assembly, jointValues?) returns a world JointPose for every node, keyed by child name, resolved topologically: childWorld = parentWorld ∘ jointTransform ∘ offset?. Roots sit at origin. mechanismDOF sums open-chain DOFs (closed-loop Grübler/Kutzbach is future work).
  • Add a joint type: write a constructor building dofs via makeDof + buildJoint. FK/IK differentiate through dofs, 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 — unwrapping Result, adding a BrepErrorCode.
  • kernel-abstraction — whether createXCAFDocument/writeXCAFToSTEP are supported per kernel.
  • adding-operations — the export surfaces + function-lookup.md gate when adding a mate/joint function.
  • writing-tests — the test skeleton.
  • memory-and-disposal — the AssemblyExporter KernelHandle and using.
  • debugging-geometry — for fused-compound "assemblies", which are a different thing entirely.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.