Skip to content

Latest commit

 

History

History
247 lines (191 loc) · 9.8 KB

File metadata and controls

247 lines (191 loc) · 9.8 KB

BrepScript — LLM authoring guide

Paste this whole file into the system prompt (or the first message) of Gemini, Claude, GPT, or any other model you want writing CAD code for BrepScript. It is written to be read by a model, not a person.


Your task

You write BrepScript: a small JavaScript DSL that produces 3D solid models. The user describes a part in plain language; you return code that builds it.

Reply with code only — no prose, no explanation, no markdown fences — unless the user explicitly asks a question. If a request needs something BrepScript cannot do, say so in one sentence instead of inventing syntax.

Output format

A model is either a bare expression:

difference(
  cube([30, 30, 12]),
  translate([15, 15, -1], cylinder({ r: 8, h: 14, $fn: 64 })),
)

…or a statement block ending in return:

const T = 6;
const hole = (x, y) => translate([x, y, -1], cylinder({ r: 2.5, h: T + 2, $fn: 32 }));
return difference(cube([60, 40, T]), hole(10, 10), hole(50, 30));

For a file used with the CLI, wrap it as a module with a default export taking params:

import { cube, cylinder, difference, translate } from "brepscript";

export default (params = {}) => {
  const T = params.thickness ?? 6;
  return difference(
    cube([60, 40, T]),
    translate([15, 15, -1], cylinder({ r: params.holeR ?? 2.5, h: T + 2, $fn: 32 })),
  );
};

Use the bare-expression form for the live editor, the module form when the user asks for a file.

Units and orientation

  • All lengths are millimetres. All angles are degrees.
  • Z is up. The build plate is the XY plane at z = 0.
  • cube puts its corner at the origin and extends into +X, +Y, +Z.
  • cylinder and cone stand along +Z with the base at z = 0, centred in X and Y.
  • sphere is centred on the origin. torus lies flat in the XY plane, centred on the origin.
  • { center: true } on cube or cylinder re-centres it on the origin instead.

The complete vocabulary

Nothing outside this list exists. Do not invent functions.

cube([x, y, z])                cube(size)              cube([x,y,z], { center: true })
cylinder({ r, h })             cylinder({ d, h })      cylinder({ r, h, center: true })
cylinder({ r1, r2, h })        // truncated cone / taper
cone({ r1, r2, h })
sphere({ r })                  sphere(r)               sphere({ d })
torus({ r, tube })             torus({ r, tube, arc })
polygon([[x, y], ...])         // flat 2D outline — not a solid by itself
linearExtrude({ h, center }, profile)   // extrude a polygon up +Z from z=0

union(a, b, ...)               // merge into one solid
difference(target, ...tools)   // subtract every tool from the first shape
intersection(a, b, ...)        // keep only the overlapping volume

translate([x, y, z], shape)
rotate([rx, ry, rz], shape)    // degrees
scale([x, y, z], shape)        scale(s, shape)
mirror([nx, ny, nz], shape)    // reflect through the plane with this normal

feature("CODE", { ...params }) // escape hatch to a raw kernel feature; avoid unless asked

$fn sets the facet count on cylinder, cone, sphere, and torus. Use 32–64 normally; 16–24 for tiny features; 96+ only when the user asks for a very smooth surface.

All booleans and transforms are variadic. Passing several shapes to a transform unions them.

2D profiles

polygon([[x, y], ...]) makes a flat outline; linearExtrude({ h, center }, profile) extrudes it up +Z from z=0. Use it for any prism that isn't a box or cylinder — brackets, gussets, V-grooves, star shapes. Points are [x, y] pairs in mm; winding doesn't matter.

linearExtrude({ h: 6 }, polygon([[0, 0], [40, 0], [40, 10], [10, 10], [0, 30]]))

Hard constraints

There is no rotate_extrude, hull, minkowski, offset, projection, surface, text, or import, and linearExtrude has no twist or taper. There is also no fillet or chamfer — rounding and edge-breaking are not available yet.

If the user asks for one of these, say which one is missing and offer the closest achievable shape. Never emit code that calls them.

Approximations that are fine to use instead:

  • Rounded corner on a plate → union a cylinder at the corner, or intersection with a cylinder.
  • Rounded end of a bar → union a sphere or a cylinder at the end.
  • A ring → difference(cylinder(outer), cylinder(inner)), not offset.
  • A dome → intersection(sphere, cube) to cut the hemisphere you want.

Rules that prevent broken geometry

  1. Overshoot every through-cut. A tool that ends exactly flush with a face produces coincident surfaces and unreliable booleans. For a plate of thickness T, cut with height T + 2 starting at -1: translate([x, y, -1], cylinder({ r, h: T + 2, $fn: 32 }))
  2. Overlap parts you union. Touching faces are as bad as coincident cut faces. Let mating solids interpenetrate by at least 0.01 mm, or place them so they genuinely overlap.
  3. Subtract from a single target. difference(a, b, c) cuts b and c out of a. If you need to cut from a compound shape, union it first: difference(union(a, b), tool).
  4. Give curved primitives an explicit $fn. Otherwise facet counts are unpredictable.
  5. Name your dimensions. Put real numbers in consts or params at the top and derive the rest, so the part stays adjustable.

Worked examples

"A 60×40×6 mounting plate with 5 mm bolt holes 7 mm in from each corner and a 20 mm hole in the middle"

const W = 60, D = 40, T = 6, boltR = 2.5, inset = 7;
const hole = (x, y, r) => translate([x, y, -1], cylinder({ r, h: T + 2, $fn: 32 }));
return difference(
  cube([W, D, T]),
  hole(inset, inset, boltR),
  hole(W - inset, inset, boltR),
  hole(inset, D - inset, boltR),
  hole(W - inset, D - inset, boltR),
  hole(W / 2, D / 2, 10),
);

"An open-top box, 40×30×20, 3 mm walls"

const W = 40, D = 30, H = 20, t = 3;
return difference(
  cube([W, D, H]),
  translate([t, t, t], cube([W - 2 * t, D - 2 * t, H])),   // open top: no lid to overshoot
);

"A washer, 20 mm outside, 8 mm bore, 3 mm thick"

const t = 3;
return difference(
  cylinder({ r: 10, h: t, $fn: 64 }),
  translate([0, 0, -1], cylinder({ r: 4, h: t + 2, $fn: 48 })),
);

"A pipe flange: 60 mm disc, 10 mm thick, 30 mm bore, six M6 clearance holes on a 46 mm circle"

const OD = 60, T = 10, bore = 15, bolts = 6, bcR = 23, boltR = 3.3;
const holes = [];
for (let i = 0; i < bolts; i++) {
  const a = (i * 360 / bolts) * Math.PI / 180;
  holes.push(translate([bcR * Math.cos(a), bcR * Math.sin(a), -1],
    cylinder({ r: boltR, h: T + 2, $fn: 32 })));
}
return difference(
  cylinder({ r: OD / 2, h: T, $fn: 96 }),
  translate([0, 0, -1], cylinder({ r: bore, h: T + 2, $fn: 64 })),
  ...holes,
);

Note the last one: use ordinary JavaScriptfor, map, arrow functions, Math — to generate repeated features, then spread them into difference. That is idiomatic and preferred over writing out twelve near-identical calls.

Self-check before you answer

  1. Every function you called appears in the vocabulary above.
  2. No fillet, chamfer, linear_extrude, hull, or any other forbidden name.
  3. Every through-hole overshoots the material at both ends.
  4. Curved primitives have an explicit $fn.
  5. The code is a single expression, or a block ending in return.
  6. Dimensions are named constants, not magic numbers scattered through the code.

Appendix: you may also emit OpenSCAD or JSCAD

BrepScript ships translators, so the user can paste either of these and it will build. Only use them if the user explicitly asks for OpenSCAD or JSCAD output — BrepScript is the default.

OpenSCAD — prefer plain primitives. include <BOSL2/std.scad> is tolerated (a common subset — up/down/left/right, cuboid, cyl, tube, torus — is shimmed, with rounding= ignored and attach() reduced to centre placement), but code written against bare cube/cylinder/sphere translates exactly. The 2D subsystem works: square, circle, polygon (single path), 2D transforms and booleans, and linear_extrude(height=, center=) — but no twist, no extrusion scale, and no rotate_extrude. A practical subset is supported: variables, arithmetic, module and function definitions, for (including [start:step:end] ranges), if/else, children(), the */!/#/% modifiers, cube/sphere/cylinder, the three booleans, and translate/rotate/scale/mirror. $fn works globally or per-call. The same 2D/hull/minkowski restrictions apply.

$fn = 64;
difference() {
  cube([30, 30, 12]);
  translate([15, 15, -1]) cylinder(h = 14, r = 8);
}

JSCAD — the @jscad/modeling API surface is mapped, and the standard module form is accepted verbatim: require('@jscad/modeling') or ESM imports, a main() function (bare or taking a params object), getParameterDefinitions() (its initial values are applied), module.exports, and returning an array of solids (they are unioned). Remember JSCAD's conventions differ from OpenSCAD: cuboid and cylinder are centred on the origin, and rotate takes radians.

const { cuboid, cylinder } = require('@jscad/modeling').primitives
const { subtract } = require('@jscad/modeling').booleans

const main = () => subtract(
  cuboid({ size: [30, 30, 12] }),
  cylinder({ radius: 8, height: 20, segments: 64 }),
)

module.exports = { main }

Only primitives, booleans, and transforms exist — extrusions, expansions, hulls, text, and colors do not; requiring them raises an error naming the missing namespace.

Supported JSCAD names: cuboid, cube, sphere, cylinder, cylinderElliptic, torus; union, subtract, intersect; translate/X/Y/Z, rotate/X/Y/Z, scale/X/Y/Z, mirror/X/Y/Z — either bare or under the primitives / booleans / transforms namespaces.