API for LLMs
Everything a model needs to write sand3d: the rules of the vocabulary, every signature with one line on it, and worked examples. 3 643 tokens (o200k_base). Paste it in a system prompt as it is.
import sand3d as s3d; mm, or units("m") first. Makers take a name, return the solid; edits return it. Edges and faces go by their maker's names (top bottom side rim seam start end cut, tool names, plate.top).
at sets the values it is given and keeps the others; on= changes the frame, and x, y, z then run along its axes; transform moves the whole thing, as a parent would, mirror included.on=: once per frame, in at, hole and pocket.copy keeps it; always use what at returns.box is the extrude of its rect; extrude(both=True) centres one on its plane. A face's frame starts where the origin projects onto it: a vertical face's, at its foot. A frame's Z is the face's normal; on a face whose normal is ±Z its X is +X, on any other X is Z × normal: on a face looking +X, X is +Y and Y is up. A side, an edge or a corner of a box is named by directions: bbox(ob, "+X") is the centre of that side of its bounding box, "+X-Y" the middle of an edge, "+X-Y+Z" a corner, with the axes of a face looking the first way named, on a part or a group; at's anchor takes the same ("-Z", "+Y-Z"; its words top bottom left right front back are +Z -Z -X +X -Y +Y).joint does not move it, it says how it moves — on the part its frame on was read on (ob.face, bbox, local, an assembly's anchor; it stays read on it through .move, .turn, .offset), about or along axis: a direction ("+Z", "-X"…) in the space the parent lives in, as in face, or "normal", on's Z. A pivot given as numbers — a source's joint origin, xyz and rpy — is local(parent, x, y, z, rx, ry, rz), in the parent's own space: at(child, on=…) places the child there, joint hangs it there. Positive values slide along the axis, or turn by the right hand about it: about +Z, +X turns toward +Y — a door hinged on the -Y edge of a +X face opens outward about "-Z". The range contains 0; a joint that follows another takes its range from it. Everything is built at rest, every joint at 0; pose(child, value) moves the mechanism. A part with no joint moves with its group; a jointed part moves by its joint, whatever group it is in; on is never read on the child itself. Joints may come before or after group/assembly. copy copies a jointed part's joints (it hangs on the same parent); transform moves the part with its pivot (a mirror image moves as the image of the motion); at refuses it. Two joints on one child compose in order (a screw: a turn, and a slide that follows it with ratio=pitch / 360).rect(w, d, r=0, chamfer=0) # Centred; corners rounded or cut circle(d) # Centred ellipse(w, d) # Centred slot(l, d) # Round-ended slot along X polygon(n, d) # Regular polygon, corner on +X poly(pts, fillet=0, chamfer=0) # Outline through points, arcs, splines arc(center=(0, 0), r=1, start=0, end=360, *, through=None, to=None) # Arc from start to end, anticlockwise if end > start: a path, or a poly run tangent_arc(to) # Poly run: arc leaving tangent spline(pts, closed=False, tangents=None, loop=False) # Smooth curve; closed: a profile; loop: a ring path superellipse(w, d, n=2, taper=0) # Ellipse (n=2) to box (n=4); taper narrows its +Y side, an egg station(z, w, d, x=0, y=0, n=2, taper=0) # Loft section at height z; n and taper as superellipse text(s, size, font=None) # Centred outline offset(p, t) # Grown by t stroke(path, t) # A band t wide along an open path, centred on it: a closed outline wrap(*profiles) # Convex outline round profiles mirror(p, flip='x') # Plus its image, flip negated
16 lines · 348 tokens (o200k_base)
extrude(name, profile, h=None, taper=0, twist=0, both=False, h2=None, on=None, until=None) # Prism up Z (taper > 0 narrows it upward), or until a plane or solid; on a face, by on revolve(name, profile, angle=360) # X radius, Y height, turned about Z sweep(name, profile, path, scale=1, twist=0, frenet=False) # Profile along a path loft(name, *sections, follow=False, loop=False) # Through placed sections, capped; loop: a ring body(name, plan, top, bottom, n=2) # Solid along Y from its views, where they draw it: plan (y, width), side top and bottom (y, z) or numbers; superellipse sections wire(name, path, d, closed=None, fillet=0) # Round wire along a path: (x, y, z) points, a spline, an arc, a helix; fillet rounds the corners of points, d / 2 at least helix(d, pitch, h, left=False) # Coil path up Z box(name, w, d, h) # On Z = 0, centred in X and Y cylinder(name, d, h) # Along Z, from Z = 0 tube(name, d, d_in, h) # Along Z, from Z = 0 cone(name, d, h, d2=0) # Along Z, from Z = 0 sphere(name, d) # On Z = 0 wedge(name, w, d, h, top=0) # Box narrowed on top torus(name, d, d_wire) # Ring about Z, on Z = 0 dome(name, d, h) # Cap on Z = 0
15 lines · 416 tokens (o200k_base)
cut(ob, *tools) # Subtract tools join(ob, *others) # Fuse others in; ob's material wins: group keeps each its own intersect(ob, tool) # Keep the common part split(ob, frame, keep='above') # By a frame's plane delete(ob) # Remove it copy(ob) # An independent copy, its joints with it hole(ob, d, on=(0, 0), depth=None, cbore=None, csk=None) # On frames or points: through, or depth deep pocket(ob, profile, depth=None, on=None) # Profile cut in from a face merge(*obs) # No boolean scale(ob, s) # About its origin fillet(ob, r, edges='all') # Round named edges chamfer(ob, d, edges='all', d2=None, angle=None) # Bevel named edges soften(ob, r, angle=30) # Round all sharp edges shell(ob, t, open=None) # Hollow; open named faces, or the flat faces looking "+Y", "-X +X" dent(ob, points, r, depth) # Press hollows at points
15 lines · 267 tokens (o200k_base)
Frame(origin=(0, 0, 0), normal=(0, 0, 1), up=None) # A place to build on .offset(d) .turn(rx=0, ry=0, rz=0) .move(x=0, y=0, z=0) .point(x=0, y=0, z=0) ob.face(which) # Frame of a named face, or furthest along "+Z" ob.edge(which) # Path along the edges named which, in the space ob is in: the longest run (as long: the one reaching furthest -X, then -Y, -Z), closed if it closes bbox(ob, which) # Frame on its bounding box: a side "+X", an edge "+X-Y", a corner "+X-Y+Z" local(ob, x=0, y=0, z=0, rx=0, ry=0, rz=0) # Frame at (x, y, z) of its own space, turned (rx, ry, rz) degrees at(ob, x=None, y=None, z=None, rx=None, ry=None, rz=None, on=None, anchor=None) # Place it: set what is given, keep the rest; anchor: "-Z", "+Y-Z"… transform(ob, x=0, y=0, z=0, rx=0, ry=0, rz=0, mirror=None) # Move it by, as a parent would; its placement, not its mesh grid(nx, ny=1, px=0, py=0, stagger=False, on=None) # On a grid ring(n, r, start=0, angle=360, on=None) # On a circle along(path, pitch=None, count=None) # Along a path snap(targets, points, direction=None) # On a surface
12 lines · 405 tokens (o200k_base)
gear(name, teeth, module, width, bore=0, angle=20) # Involute spur gear knurl(name, d, h, pitch=None, straight=False) # Knurled grip; pitch 1 mm by default thread(name, d, length, pitch, left=False) # Threaded rod
3 lines · 70 tokens (o200k_base)
material(ob, kind, color=None, roughness=None, metallic=None, opacity=None, emission=None) # Kind: oak walnut pine birch steel brushed_steel chrome aluminium brass copper painted_metal plastic glossy_plastic rubber glass ceramic fabric velvet leather marble concrete light; color "#rrggbb"
1 lines · 61 tokens (o200k_base)
joint(name, child, on, kind='revolute', axis='normal', range=None, follows=None, ratio=1) # How child moves on the part on was read on: revolute (range in degrees), continuous (no range), prismatic (range in the unit), free (no axis, no range); follows: a child or a joint name, and this joint's value is ratio × its (degrees per unit, or units per degree), no range; returns child pose(ob, value, joint=None) # Move the mechanism: that joint to value (degrees or the unit), an error outside its range; what it carries follows; joint= when child has two seat(host, part) # Take out of host the volume part fills, as it sits at rest; part untouched
3 lines · 168 tokens (o200k_base)
group(name, *children) # Parent of children assembly(name, *children, **anchors) # Parts and anchors attach(handle, name, *children) # A detail, under a handle Handle(root, **anchors) # Root and anchors export(root, path) # root and what hangs on it by its joints, in metres: .glb at the current pose, joints in extras; .urdf a link per moving part, at rest, meshes beside it; .usda the URDF's tree as OpenUSD text, with the materials; .obj and .stl its meshes at the current pose empty_scene() # Fresh scene units(name=None) # Length unit: "mm" (default), "cm", "m" fingerprint() # Digest of the source
8 lines · 172 tokens (o200k_base)
def bracket(w=80, d=40, t=6, hole_d=6.6, pitch=50): plate = s3d.extrude("plate", s3d.rect(w, d, r=4), t) s3d.hole(plate, hole_d, on=s3d.grid(2, 1, pitch, 0)) s3d.fillet(plate, 1, edges="top") return plate def lamp(stem_h=300): base = s3d.revolve("base", [(0, 0), (70, 0), (60, 20), (0, 20)]) stem = s3d.at(s3d.cylinder("stem", 12, stem_h), on=base.face("top")) lamp = s3d.assembly("lamp", base, stem, head=s3d.Frame((0, 0, 20 + stem_h))) add_shade(lamp) return lamp def add_shade(lamp, d=180): shade = s3d.shell(s3d.cone("shade", d, 120, d2=d / 2), 1.5, open="top bottom") s3d.attach(lamp, "shade", s3d.at(shade, on=lamp.head)) def rack(n=4, pitch=30, depth=40, t=5): base = s3d.extrude("base", s3d.rect(n * pitch, depth, r=3), t) pegs = s3d.at(s3d.cylinder("peg", 6, 20), on=s3d.grid(n, 1, pitch), z=t) # a copy per frame hook = s3d.at(s3d.extrude("hook", s3d.rect(4, 12), 12), x=n * pitch / 2 - 4, z=t) image = s3d.transform(s3d.copy(hook), mirror="x") # its image across x = 0 s3d.join(base, pegs, hook, image) # a list is its elements s3d.hole(base, 4, on=s3d.grid(2, 1, (n - 1) * pitch).move(y=depth / 2 - 6)) s3d.chamfer(base, 0.6, edges="peg") return base def gate(w=750, h=1200, post=100): l_post = s3d.at(s3d.box("post", post, post, h + 300), x=-w - post / 2) r_post = s3d.transform(s3d.copy(l_post), mirror="x") board = s3d.at(s3d.box("board", w, 40, h), x=-w / 2, z=150) # from the post to x = 0 handle = s3d.at(s3d.cylinder("handle", 20, 60), on=s3d.bbox(board, "+Y").move(x=60 - w / 2)) # near x = 0 leaf = s3d.group("leaf", board, handle) # the handle moves with it s3d.joint("hinge", leaf, s3d.bbox(l_post, "+X+Y"), axis="+Z", range=(0, 90)) # the post's edge it opens toward: +X turns toward +Y twin = s3d.transform(s3d.copy(leaf), mirror="x") # its image, hinged at x = +w: opens toward +Y too gate = s3d.assembly("gate", l_post, r_post, leaf, twin) s3d.pose(leaf, 60) return gate
38 lines · 837 tokens (o200k_base)
# sand3d
`import sand3d as s3d`; mm, or `units("m")` first. Makers take a name, return the solid; edits return
it. Edges and faces go by their maker's names (top bottom side rim seam start end cut, tool names,
plate.top).
1. A placement is a frame plus values. `at` sets the values it is given and keeps the others; `on=`
changes the frame, and x, y, z then run along its axes; `transform` moves the whole thing, as a parent
would, mirror included.
2. To repeat, pass a list of frames to `on=`: once per frame, in `at`, `hole` and `pocket`.
3. A list of objects is worth its elements, wherever a verb takes objects.
4. What a verb uses up no longer exists. `copy` keeps it; always use what `at` returns.
5. Every solid starts at z = 0, centred in x and y: `box` is the `extrude` of its `rect`; `extrude(both=True)`
centres one on its plane. A face's frame starts where the origin projects onto it: a vertical face's, at
its foot. A frame's Z is the face's normal; on a face whose normal is ±Z its X is +X, on any other
X is Z × normal: on a face looking +X, X is +Y and Y is up. A side, an edge or a corner of a box is
named by directions: `bbox(ob, "+X")` is the centre of that side of its bounding box, `"+X-Y"` the
middle of an edge, `"+X-Y+Z"` a corner, with the axes of a face looking the first way named, on a part
or a group; `at`'s `anchor` takes the same ("-Z", "+Y-Z"; its words top bottom left right front back
are +Z -Z -X +X -Y +Y).
6. A joint lets a part move on another. Place the part first: `joint` does not move it, it says how it
moves — on the part its frame `on` was read on (`ob.face`, `bbox`, `local`, an assembly's anchor; it
stays read on it through `.move`, `.turn`, `.offset`), about or along `axis`: a direction ("+Z", "-X"…)
in the space the parent lives in, as in `face`, or "normal", `on`'s Z. A pivot given as numbers — a
source's joint origin, xyz and rpy — is `local(parent, x, y, z, rx, ry, rz)`, in the parent's own space:
`at(child, on=…)` places the child there, `joint` hangs it there. Positive values slide along the axis,
or turn by the right hand about it: about +Z, +X turns toward +Y — a door hinged on the -Y edge of a +X
face opens outward about "-Z". The range contains 0; a joint that follows another takes its range from
it. Everything is built at rest, every joint at 0; `pose(child, value)` moves the mechanism. A part with
no joint moves with its group; a jointed part moves by its joint, whatever group it is in; `on` is never
read on the child itself. Joints may come before or after `group`/`assembly`. `copy` copies a jointed
part's joints (it hangs on the same parent); `transform` moves the part with its pivot (a mirror image
moves as the image of the motion); `at` refuses it. Two joints on one child compose in order (a screw: a
turn, and a slide that follows it with `ratio=pitch / 360`).
```python
# profiles
rect(w, d, r=0, chamfer=0) # Centred; corners rounded or cut
circle(d) # Centred
ellipse(w, d) # Centred
slot(l, d) # Round-ended slot along X
polygon(n, d) # Regular polygon, corner on +X
poly(pts, fillet=0, chamfer=0) # Outline through points, arcs, splines
arc(center=(0, 0), r=1, start=0, end=360, *, through=None, to=None) # Arc from start to end, anticlockwise if end > start: a path, or a poly run
tangent_arc(to) # Poly run: arc leaving tangent
spline(pts, closed=False, tangents=None, loop=False) # Smooth curve; closed: a profile; loop: a ring path
superellipse(w, d, n=2, taper=0) # Ellipse (n=2) to box (n=4); taper narrows its +Y side, an egg
station(z, w, d, x=0, y=0, n=2, taper=0) # Loft section at height z; n and taper as superellipse
text(s, size, font=None) # Centred outline
offset(p, t) # Grown by t
stroke(path, t) # A band t wide along an open path, centred on it: a closed outline
wrap(*profiles) # Convex outline round profiles
mirror(p, flip='x') # Plus its image, flip negated
# solids, paths
extrude(name, profile, h=None, taper=0, twist=0, both=False, h2=None, on=None, until=None) # Prism up Z (taper > 0 narrows it upward), or until a plane or solid; on a face, by on
revolve(name, profile, angle=360) # X radius, Y height, turned about Z
sweep(name, profile, path, scale=1, twist=0, frenet=False) # Profile along a path
loft(name, *sections, follow=False, loop=False) # Through placed sections, capped; loop: a ring
body(name, plan, top, bottom, n=2) # Solid along Y from its views, where they draw it: plan (y, width), side top and bottom (y, z) or numbers; superellipse sections
wire(name, path, d, closed=None, fillet=0) # Round wire along a path: (x, y, z) points, a spline, an arc, a helix; fillet rounds the corners of points, d / 2 at least
helix(d, pitch, h, left=False) # Coil path up Z
box(name, w, d, h) # On Z = 0, centred in X and Y
cylinder(name, d, h) # Along Z, from Z = 0
tube(name, d, d_in, h) # Along Z, from Z = 0
cone(name, d, h, d2=0) # Along Z, from Z = 0
sphere(name, d) # On Z = 0
wedge(name, w, d, h, top=0) # Box narrowed on top
torus(name, d, d_wire) # Ring about Z, on Z = 0
dome(name, d, h) # Cap on Z = 0
# edits
cut(ob, *tools) # Subtract tools
join(ob, *others) # Fuse others in; ob's material wins: group keeps each its own
intersect(ob, tool) # Keep the common part
split(ob, frame, keep='above') # By a frame's plane
delete(ob) # Remove it
copy(ob) # An independent copy, its joints with it
hole(ob, d, on=(0, 0), depth=None, cbore=None, csk=None) # On frames or points: through, or depth deep
pocket(ob, profile, depth=None, on=None) # Profile cut in from a face
merge(*obs) # No boolean
scale(ob, s) # About its origin
fillet(ob, r, edges='all') # Round named edges
chamfer(ob, d, edges='all', d2=None, angle=None) # Bevel named edges
soften(ob, r, angle=30) # Round all sharp edges
shell(ob, t, open=None) # Hollow; open named faces, or the flat faces looking "+Y", "-X +X"
dent(ob, points, r, depth) # Press hollows at points
# frames, placement
Frame(origin=(0, 0, 0), normal=(0, 0, 1), up=None) # A place to build on
.offset(d) .turn(rx=0, ry=0, rz=0) .move(x=0, y=0, z=0) .point(x=0, y=0, z=0)
ob.face(which) # Frame of a named face, or furthest along "+Z"
ob.edge(which) # Path along the edges named which, in the space ob is in: the longest run (as long: the one reaching furthest -X, then -Y, -Z), closed if it closes
bbox(ob, which) # Frame on its bounding box: a side "+X", an edge "+X-Y", a corner "+X-Y+Z"
local(ob, x=0, y=0, z=0, rx=0, ry=0, rz=0) # Frame at (x, y, z) of its own space, turned (rx, ry, rz) degrees
at(ob, x=None, y=None, z=None, rx=None, ry=None, rz=None, on=None, anchor=None) # Place it: set what is given, keep the rest; anchor: "-Z", "+Y-Z"…
transform(ob, x=0, y=0, z=0, rx=0, ry=0, rz=0, mirror=None) # Move it by, as a parent would; its placement, not its mesh
grid(nx, ny=1, px=0, py=0, stagger=False, on=None) # On a grid
ring(n, r, start=0, angle=360, on=None) # On a circle
along(path, pitch=None, count=None) # Along a path
snap(targets, points, direction=None) # On a surface
# parts
gear(name, teeth, module, width, bore=0, angle=20) # Involute spur gear
knurl(name, d, h, pitch=None, straight=False) # Knurled grip; pitch 1 mm by default
thread(name, d, length, pitch, left=False) # Threaded rod
# materials
material(ob, kind, color=None, roughness=None, metallic=None, opacity=None, emission=None) # Kind: oak walnut pine birch steel brushed_steel chrome aluminium brass copper painted_metal plastic glossy_plastic rubber glass ceramic fabric velvet leather marble concrete light; color "#rrggbb"
# articulation
joint(name, child, on, kind='revolute', axis='normal', range=None, follows=None, ratio=1) # How child moves on the part on was read on: revolute (range in degrees), continuous (no range), prismatic (range in the unit), free (no axis, no range); follows: a child or a joint name, and this joint's value is ratio × its (degrees per unit, or units per degree), no range; returns child
pose(ob, value, joint=None) # Move the mechanism: that joint to value (degrees or the unit), an error outside its range; what it carries follows; joint= when child has two
seat(host, part) # Take out of host the volume part fills, as it sits at rest; part untouched
# assembly
group(name, *children) # Parent of children
assembly(name, *children, **anchors) # Parts and anchors
attach(handle, name, *children) # A detail, under a handle
Handle(root, **anchors) # Root and anchors
export(root, path) # root and what hangs on it by its joints, in metres: .glb at the current pose, joints in extras; .urdf a link per moving part, at rest, meshes beside it; .usda the URDF's tree as OpenUSD text, with the materials; .obj and .stl its meshes at the current pose
empty_scene() # Fresh scene
units(name=None) # Length unit: "mm" (default), "cm", "m"
fingerprint() # Digest of the source
```
```python
def bracket(w=80, d=40, t=6, hole_d=6.6, pitch=50):
plate = s3d.extrude("plate", s3d.rect(w, d, r=4), t)
s3d.hole(plate, hole_d, on=s3d.grid(2, 1, pitch, 0))
s3d.fillet(plate, 1, edges="top")
return plate
def lamp(stem_h=300):
base = s3d.revolve("base", [(0, 0), (70, 0), (60, 20), (0, 20)])
stem = s3d.at(s3d.cylinder("stem", 12, stem_h), on=base.face("top"))
lamp = s3d.assembly("lamp", base, stem, head=s3d.Frame((0, 0, 20 + stem_h)))
add_shade(lamp)
return lamp
def add_shade(lamp, d=180):
shade = s3d.shell(s3d.cone("shade", d, 120, d2=d / 2), 1.5, open="top bottom")
s3d.attach(lamp, "shade", s3d.at(shade, on=lamp.head))
def rack(n=4, pitch=30, depth=40, t=5):
base = s3d.extrude("base", s3d.rect(n * pitch, depth, r=3), t)
pegs = s3d.at(s3d.cylinder("peg", 6, 20), on=s3d.grid(n, 1, pitch), z=t) # a copy per frame
hook = s3d.at(s3d.extrude("hook", s3d.rect(4, 12), 12), x=n * pitch / 2 - 4, z=t)
image = s3d.transform(s3d.copy(hook), mirror="x") # its image across x = 0
s3d.join(base, pegs, hook, image) # a list is its elements
s3d.hole(base, 4, on=s3d.grid(2, 1, (n - 1) * pitch).move(y=depth / 2 - 6))
s3d.chamfer(base, 0.6, edges="peg")
return base
def gate(w=750, h=1200, post=100):
l_post = s3d.at(s3d.box("post", post, post, h + 300), x=-w - post / 2)
r_post = s3d.transform(s3d.copy(l_post), mirror="x")
board = s3d.at(s3d.box("board", w, 40, h), x=-w / 2, z=150) # from the post to x = 0
handle = s3d.at(s3d.cylinder("handle", 20, 60), on=s3d.bbox(board, "+Y").move(x=60 - w / 2)) # near x = 0
leaf = s3d.group("leaf", board, handle) # the handle moves with it
s3d.joint("hinge", leaf, s3d.bbox(l_post, "+X+Y"), axis="+Z", range=(0, 90)) # the post's edge it opens toward: +X turns toward +Y
twin = s3d.transform(s3d.copy(leaf), mirror="x") # its image, hinged at x = +w: opens toward +Y too
gate = s3d.assembly("gate", l_post, r_post, leaf, twin)
s3d.pose(leaf, 60)
return gate
```