Author a brepjs part
SkillDev toolsUse when authoring or editing a brepjs `.brep.ts` part — writing the geometry with the functional API (box, cylinder, fuse, cut, fillet, sketch→extrude…), declaring an `expected` block, and following the hard rules (import every function, unwrap Results, select edges, coordinate semantics). Also covers buildings/BIM/IFC via the declarative family layer (references/families-bim.md). This is the authoring step; pair it with brepjs:verify to check the result.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Author a brepjs part skill
What this skill tells your AI
The instructions your AI receives, as published by andymai/brepjs in packages/brepjs-cad/skills/implement/SKILL.md and read by ahel’s review.
You write a .brep.ts part; the brep CLI runs it on a geometry kernel and reports what it
measured. Judge the part by the report (see brepjs:verify), not by how the code reads. This skill
is self-contained — everything needed to author correctly is here or in references/.
The CLI ships in the brepjs-cad package as brep. Installed: brep verify part.brep.ts ….
Otherwise: npx -y -p brepjs-cad brep verify ….
Authoring contract
export default () => <shape>(orasync () => {…}toawait loadFont/importSTEP).- Short functional API (
box,cylinder,fuse,cut,fillet, …), named consts at the top. - Scaffold with
brep init <name>. Edit source, never generated artifacts (STEP/STL/GLB derive from the.brep.ts).
Realize the designed object — not just a valid one
--check passing means buildable, not correct. The bar is the part the brief names, recognisable
as that designed object — and the way that fails is simplification: a valid, generic version that
drops the one feature that makes it itself.
- Decompose the brief into named features, then mark the ONE defining feature — the geometry without
which it's a generic blob.
GT2 pulley→ the belt-tooth profile (a smooth groove is not a GT2 pulley);fluted knob→ full-height flutes around the whole perimeter (scattered scallops are not flutes);twisted impeller/swept fan→ a cambered airfoil section extruded radially with a pitch twist (references/airfoils.md), not flat blades ortwistAngleon a paddle;scroll chuck→ the spiral face groove;involute gear→ the tooth flank (references/gears.md). - Build that feature to spec, not an eyeballed approximation. Each of the above passes
--checkwhile missing its headline feature — a blob that verifies. If the feature needs real math (gear/thread/involute/ scroll), use the reference recipe; don't substitute a smooth or sparse stand-in. - Before finishing, re-read the brief noun by noun and confirm each named feature is actually in the geometry — including count (e.g. four mount holes, N teeth), not just present-ish.
Choose the operation (reliability tiers)
Prefer ops that succeed first-try; lean on the report and small steps for advanced ops. Full table:
references/operation-tiers.md. In short: primitives, booleans, compound, sketch→extrude,
fillet, shell/offset, transforms are reliable; sweeps/lofts/revolves/fuseAll/text are
advanced; chamfer is the fragile exception (prefer fillet).
Declare intent — the expected block
Add export const expected = { … } from the brief; the CLI asserts it, catching valid-but-wrong
sizing. The only authorable keys are volume, area, bounds, tolerancePct (each optional;
tolerancePct sets the match window) — TOP_LEVEL_KEYS in src/verify/expected.ts:45. Bounds
shape is exactly { xMin, xMax, yMin, yMax, zMin, zMax } (any subset) — not { min, max } or
{ x, y, z } (a wrong shape reports EXPECTED_UNKNOWN_KEY). shapeType is report-only, not
authorable: the report tells you whether the part measured as a solid/compound/etc., but
putting shapeType (or any other field) in expected also reports EXPECTED_UNKNOWN_KEY — assert
the body count or shape via volume/bounds, never a shapeType key.
Prefer bounds over a hand-computed volume (a wrong number fails a correct part). Predict
only extents you place directly — a footprint, where each body sits, the flat face of a body you
placed there — these read off your datums and catch a dropped/misplaced body. An extent governed by a
rotation, a part's orientation, a proud sub-feature, a half-space clip, or the outer top/bottom of a
deep multi-body stack is not a datum: bound it generously or measure-first (run once, copy the
report's measured value). That last one is the #1 EXPECTED_ASSERTION_FAILED on assemblies — a
stack's extreme z is usually crowned by a rounded/proud feature (a carrier hub, a ball cap) and sums
every body's placement error, so measure it; don't hand-add the stack. A flat lid-on-base height
you place is fine; the moment a curved or proud sub-feature defines the extreme, it's governed. This
was the #1 first-try failure across the corpus (rotated handles, articulated yokes, flange discs,
clipped balls): when an operand is rotated, a disc/sphere crowns an axis, or a cut clips an
extreme, measure that one axis — don't predict it.
A chamfer/fillet only REMOVES material — it never grows the bounding box. A beveled or rounded
outer corner keeps the original face plane as its bound, so the extent stays at the un-chamfered face:
predict xMin = 0 for a corner chamfered at x = 0, never xMin = -chamfer.
An extent is a datum only if you place that face directly. A derived extent is not — and these
are the other half of the EXPECTED_ASSERTION_FAILEDs: a body translated beside another (its far
edge is offset ± its own half-extent, not the offset), a face/foot widened to overlap a neighbour
for fusing (its outer edge is the widened size, not the nominal feature length), or a cylinder/cone
given a non-default axis (its far end is base + axis·length, e.g. base x=-15, axis -X,
length 14 → xMin=-29). Compute these from the SAME const that places the geometry, or measure-first.
Hard rules
- Import every function you call. No globals — every op is a named export from
'brepjs'. A used-but-unimported symbol isTS2304: Cannot find nameand fails--checkbefore geometry runs (the #1 first-attempt failure). Re-scan the body before finishing. - Transforms are free functions, shape-first —
translate(shape, [x,y,z]),rotate(shape, deg, { axis }),mirror,scale(angles in degrees), the same shape-first form as booleans. They are NOT methods:shape.translate(...)isTS2339: Property 'translate' does not exist. Placing assembly parts needs these even when the brief doesn't shout "transform". (references/transforms.md.) - Unwrap Results. Booleans and
measureVolume/measureAreareturnResult:unwrap(cut(...))and check theErrbranch before chaining.TS2322: Result<X> is not assignable to X(on an assignment/return) — orTS2345when you feed an un-unwrappedResultstraight into another op's argument (e.g.fuse(a, cut(b, c))) — means an op (cut/fuse/fillet/chamfer/shell/…) was used withoutunwrap(). Unwrap at every step, including the final return — aResultdefault export (export default cut(...)) slips pastverify(it auto-unwraps aResultdefault export, so--checkis green) but renders nothing in a viewer/mesh path. Alwaysunwrap()the returned shape. fusewelds only where solids overlap. Bodies merely touching on a coplanar face/ring may return a looseCompound(ok:true, not one watertight solid). Overlap the operands +fuseAll(shapes, { unsafe: true })to weld; usecompoundfor a distinct-bodies assembly. For MANY operands (a grille, a stud grid, a space frame),fuseAllunsafe routinely leaves a loose N-solid compound even with overlap — fold with a pairwisefuse()reduce over real overlaps and confirmgetSolids(part).length === 1. (references/booleans.md.) But aCompoundresult is NOT a failure to chase: even genuinely overlapping operands often fuse toshapeType:Compound(ok:true, correct geometry) rather than a singleSolid— and that's fine, becauseshapeTypeis report-only/non-authorable and bounds/volume/validity still pass. Don't burn attempts trying to force aSolid; only do so (and then only worry) when a downstreamfillet/shell/offsetneeds aValidSolid(next rule). The report'snotesflags a multi-body Compound and its solid count — if a part you meant as ONE piece comes back as N bodies, the weld failed (overlap +fuseAllunsafe); a count matching a deliberate assembly is fine.fillet/chamfer/shell/offsetneed aValidSolid. Primitives already are one and booleans preserve it, so a primitive-rooted chain feeds them directly. A shape from a 2D-sketch.extrude()/.revolve()(orloft/sweep) is typedShape3D; passing it to these ops isTS2345. Lift in two steps:if (!isSolid(x)) throw …; const solid = unwrap(validSolid(x));. Or build the prism from a primitive when you know you'llfillet/shellit. (references/modifiers.md.)- A loose-
Compoundboolean can't be lifted toValidSolid— fix the boolean, don't lift. Whenfuse/fuseAllonly touch (don't overlap) they return aCompound(ok:true), andisSolidreturnsfalseon it (shapeType()==='compound', not'solid'—src/core/shapeTypes.ts:248). There is no lift from a multi-bodyCompoundto aValidSolid:validSolid()needs oneSolid, soif (!isSolid(x)) throw …just throws, and feeding the Compound onwardKERNEL_FAILEDs. Make the operands actually overlap andfuseAll(shapes, { unsafe: true })so the weld yields a single solid first, thenfillet/shell. (references/booleans.md.) - Select edges/faces; don't fillet/chamfer everything.
fillet(solid, radius)with no edge list rounds EVERY edge and frequentlyFILLET_FAILEDs. PassedgeFinder().inDirection('Z').findAll(solid).inDirectionmatches BOTH ± orientations; discriminate by position with.when(f => getBounds(f).zMax > t)(getBoundsis its own import). Finders take a direction ('X'/'Y'/'Z'/Vec3), never a plane: a top face isfaceFinder().inDirection('Z');parallelTo('XY')fails--check— use'Z'. (references/modifiers.md.) chamferis kernel-fragile.CHAMFER_FAILEDis common even with a correct edge list. Preferfillet, model the bevel additively (cutwith an angled tool), or drop it. Re-running the same chamfer rarely helps.- A through hole/slot/mortise needs a tool proud of BOTH faces. Size the cutting
cylinder/boxLONGER than the body and place it so it pokes out each end (e.g. height = wall + 2, positioned past both faces) — a tool flush with or short of a face leaves a blind pocket, not a through feature, and--checkcan't catch it (the part is still valid). The brief word "through" (or "bore", "passage") is the cue. Confirm it actually passes through with thesection/xray view, not just the exterior. revolveangle is RADIANS (Math.PI * 2= full turn). Build a revolve profile withpolygon(points3D), notdraw().close().sketchOnPlane('XZ').face()(fails--check). (references/sketching-2d.md.)box(width, depth, height)— depth is Y, height is Z, mm.atsets the geometric CENTER: barebox(w,d,h)is corner-at-origin,{ centered: true }centers on origin,{ at:[x,y,z] }centers there.cylinder/coneatis the base center;sphereatis its center.- No half-sphere primitive: clip a full
sphereto a half-space withintersect(aboxover the half you want). Fusing a whole sphere bulges past a cap face. See thedome-capexample. - Pattern angles are DEGREES (
circularPattern/rectangularPatternfullAngle), unlikerevolve(radians). Check the unit per op. - Parts may be
async:export default async () => {…}is awaited —await loadFont(...)(required before anysketchText/drawText) orawait importSTEP(...).--checktype-checks Node built-ins, so a part mayimport { readFile } from 'node:fs/promises'. - Author in ESM (the tool's default) so the kernel loads.
Reference index (load only what the task needs)
references/getting-started.md · primitives.md · sketching-2d.md · booleans.md ·
modifiers.md · transforms.md · measurement-validation.md · assemblies-motion.md ·
operation-tiers.md. Maker recipes: fdm-conventions.md · mechanical-joints.md ·
gridfinity.md · gears.md · threads.md · airfoils.md (fans/props/impellers/vanes).
Buildings/BIM/IFC: families-bim.md (declarative components → viewport meshes + IFC export).
Backstop: any symbol not covered →
reference/llms-full.txt (every export with signatures), bundled in the package.
Examples index (read the closest before authoring)
Each is a complete examples/<name>.brep.ts + <name>.expected.json baseline.
- Primitives + booleans:
mounting-bracket·flanged-coupler·transform-bracket·dome-cap. - 2D sketch → solid:
extruded-bracket·revolved-pulley·swept-gasket. - Modifiers:
rounded-block(fillet) ·chamfered-block(API shape only; chamfer is fragile) ·hollow-enclosure(shelled). - Mechanical:
spur-gear(polygon→extrude, BOSL2-faithful) ·threaded-rod(loft sections). - Gridfinity:
gridfinity-baseplate·gridfinity-bin·gridfinity-divider.
Signals
- GitHub stars
- 101
- Forks
- 9
- Last commit
- Sep 2026
- Hacker News mentions
- 20
Advanced
- Catalog kind
- skill
- Gateway key
implement-andymai- Source
- github.com/andymai/brepjs