Quickstart
sand3d is a Python library for writing 3D objects as programs. A program calls verbs that make solids, edit them and place them; Blender does the geometry. This page shows every verb with code that runs, the materials a part can be made of, articulations, the 6 rules a program follows, and how to write an assembly.
export writes metres18 programs, from a bracket to an upholstered chair. Pick one: turn it, click a part on the left, and read the code that built it on the right.
1"""A tufted chair: a buttoned back rolling over at the top, a domed seat on a nailed2apron, four tapered legs.3 4Millimetres, Z up, the chair's front towards -Y; built as its left half's legs, the5right ones mirrored. The architecture is the seat on its legs; the back and every6row of trim are details added to it: deleting `add_back` leaves a stool, and the7back's tufts follow the width they are given.8"""9import json10import math11import sys12 13import sand3d as s3d14 15 16def tufted_chair(width=520):17 chair = chair_frame(width)18 add_back(chair)19 add_seat_nails(chair)20 return chair.root21 22 23def chair_frame(width=520, depth=480, seat_z=430, inset=50, floor=0):24 seat = upholstered_seat(width, depth)25 legs = []26 for leg, y in ((front_leg, -(depth / 2 - inset)), (rear_leg, depth / 2 - inset)):27 left = s3d.at(leg(), x=-(width / 2 - inset), y=y, z=floor)28 legs += [left, s3d.transform(s3d.copy(left), mirror="x")] # the right one: its image across x = 029 return s3d.assembly("chair", s3d.at(seat.root, z=seat_z), *legs,30 seat=seat, width=width, seat_z=seat_z, rear_y=depth / 2)31 32 33def add_back(chair, behind=-30, above=200):34 back = tufted_back(chair.width)35 s3d.attach(chair, "back", s3d.at(back.root, y=chair.rear_y + behind, z=chair.seat_z + above))36 add_buttons(back)37 add_back_nails(back)38 39 40def add_seat_nails(chair, pitch=22, d=10, h=5):41 """One nail every `pitch` along the seat's seam, round its front and sides."""42 seat = chair.seat43 spots = s3d.snap(seat.parts, s3d.along(seat.seam, pitch))44 s3d.attach(seat, "seat_nails", *[s3d.at(s3d.dome("nail", d, h), on=f) for f in spots])45 46 47def add_buttons(back, d=16, h=3):48 s3d.attach(back, "buttons", *[s3d.at(s3d.dome("button", d, h), on=f) for f in back.tufts])49 50 51def add_back_nails(back, pitch=22, d=10, h=5, face=((220, -230), (226, 150)), reach=400):52 """A row up each side of the back's face, where its cover is nailed to the frame,53 from (x, z) to (x, z), found from in front, `reach` away."""54 spots = []55 for side in (-1, 1):56 (x0, z0), (x1, z1) = face57 spots += s3d.snap(back.parts, s3d.along([(side * x0, -reach, z0), (side * x1, -reach, z1)], pitch),58 direction=(0, 1, 0))59 s3d.attach(back, "back_nails", *[s3d.at(s3d.dome("nail", d, h), on=f) for f in spots])60 61 62# ---------------------------------------------------------------- parts, in the order they are called63 64def section(z, w, d, x=0, y=0, n=2, taper=0):65 """One cross-section of a loft: a superellipse `w` wide and `d` deep, squareness66 `n` (2 an ellipse, 4 a box with round corners), its +Y side narrowed by `taper`,67 centred at (x, y, z)."""68 return s3d.at(s3d.superellipse(w, d, n, taper), x=x, y=y, z=z)69 70 71def upholstered_seat(width=520, depth=480, taper=0.04, squareness=6, board=-8,72 crown=((15, 3, 3, 0), (35, 12, 10, 4), (55, 30, 24, 8), (68, 90, 72, 12), (76, 190, 160, 18)),73 apron=60, dip=15, wall=8, seam_up=6, seam_out=10):74 """A cushion domed over a board, an apron hanging round it. `crown` is the dome,75 level by level: (height, inset at the sides, inset front and back, shift back)."""76 w = width / (1 + taper) # the loft's mean width; the front edge is `width`77 cushion = s3d.loft("cushion", section(board, w, depth, n=squareness, taper=taper),78 *[section(z, w - 2 * side, depth - 2 * end, y=back, n=squareness, taper=taper)79 for z, side, end, back in crown])80 81 def drop(x): # how far the apron hangs below the board: deepest mid-front82 return apron + dip * math.cos(math.pi * x / width) ** 283 84 hem = s3d.superellipse(w - wall, depth - wall, squareness, taper).loops[0].pts85 skirt = s3d.loft("apron", *[s3d.at(s3d.superellipse(drop(x), wall, squareness), x=x, y=y, z=board - drop(x) / 2,86 ry=-90) for x, y in hem], follow=True, loop=True)87 # the seam the nails follow: just outside the apron, round the front and sides88 # but not the back, which the back of the chair covers — from the middle of the back round89 rim = s3d.superellipse(w + 2 * seam_out, depth + 2 * seam_out, squareness, taper).loops[0].pts90 start = min(range(len(rim)), key=lambda i: abs(math.atan2(rim[i][1], rim[i][0]) - math.pi / 2))91 seam = [(x, y, board - drop(x) + seam_up) for x, y in rim[start:] + rim[:start] if y < depth / 2 - seam_out]92 return s3d.assembly("seat", cushion, skirt, parts=[cushion, skirt], seam=seam)93 94 95def front_leg(height=420, profile=((0, -10, 30, 30), (210, -4, 36, 36), (420, 0, 44, 44))):96 """A square leg with round corners, tapering to its foot, which steps forward.97 `profile` is (z, y, width, depth) from the floor up; its origin is under its foot."""98 body = s3d.loft("front_leg", *[section(z * height / profile[-1][0], w, d, y=y, n=4) for z, y, w, d in profile])99 return s3d.group("front_leg", s3d.at(body, y=-profile[0][1]))100 101 102def rear_leg(height=420, profile=((0, 16, 30, 30), (210, 6, 36, 36), (420, 0, 44, 44))):103 """The back leg, raking back to its foot. Its origin is under its foot."""104 body = s3d.loft("rear_leg", *[section(z * height / profile[-1][0], w, d, y=y, n=4) for z, y, w, d in profile])105 return s3d.group("rear_leg", s3d.at(body, y=-profile[0][1]))106 107 108def tufted_back(width=520, drawn_for=520, squareness=4, profile=(109 (-260, 0, 440, 50), # foot, behind the seat: z, y, width, depth110 (-140, 6, 460, 60),111 (-20, 14, 470, 64), # the cushioned middle112 (90, 24, 480, 66),113 (170, 36, 490, 72), # leaning back114 (205, 58, 490, 70), # over the top115 (180, 82, 490, 60), # down the back116 (130, 88, 490, 46)), # tucked under117 tuft_rows=((-60, -120), (-52, -40), (-44, 40), (-34, 120)), tuft_pitch=80, tuft_margin=100, tuft_r=55,118 tuft_depth=20):119 """A cushioned panel rising from the seat and rolling over backwards, one loft120 along its own centre line; buttoned on a diamond lattice, each tuft row (y, z),121 the rows' columns staggered. The profile's widths are those of a `drawn_for` chair."""122 widen = width - drawn_for123 body = s3d.loft("back", *[section(z, w + widen, d, y=y, n=squareness) for z, y, w, d in profile], follow=True)124 reach = int((width / 2 - tuft_margin) / tuft_pitch) # columns each side of the middle125 lattice = [(j * tuft_pitch, y, z) for i, (y, z) in enumerate(tuft_rows)126 for j in range(-reach, reach + 1) if (i + j) % 2 == 0]127 tufts = s3d.snap(body, lattice)128 s3d.dent(body, tufts, tuft_r, tuft_depth)129 return s3d.assembly("tufted_back", body, parts=[body], tufts=s3d.snap(body, tufts))130 131 132if __name__ == "__main__":133 s3d.empty_scene()134 s3d.export(s3d.group("tufted_chair", tufted_chair(**json.loads(sys.argv[2] if len(sys.argv) > 2 else "{}"))),135 sys.argv[1])
135 lines · 2304 tokens (o200k_base)
1"""A chair, written as an assembly: the plan first, then the details, then the parts.2 3`chair` reads as a table of contents. `chair_frame` is the plan: the seat on its legs,4and the anchors the details need. Each `add_` function hangs one detail on those5anchors: delete its line and nothing else moves — without `add_back` it is a stool.6The parts come last, each built at its own origin, standing on z = 0.7"""8import sand3d as s3d9 10 11def chair(width=440, depth=420, seat_h=450):12 chair = chair_frame(width, depth, seat_h) # the plan13 add_back(chair) # the details, on the plan's anchors14 add_rails(chair)15 return chair.root16 17 18# ---------------------------------------------------------------- the plan19 20def chair_frame(width, depth, seat_h, t=30, inset=35):21 """The seat on four legs. Publishes the width, the seat's height and the line22 the back stands on."""23 legs = s3d.at(leg(seat_h - t), on=s3d.grid(2, 2, width - 2 * inset, depth - 2 * inset))24 seat = s3d.at(seat_board(width, depth, t), z=seat_h - t)25 return s3d.assembly("chair", seat, legs, width=width, depth=depth, seat_h=seat_h, inset=inset,26 back_y=depth / 2 - 20)27 28 29# ---------------------------------------------------------------- the details30 31def add_back(chair, h=420, t=24, button_d=16):32 """A panel standing on the seat's back edge, buttoned on its face."""33 back = s3d.at(back_panel(chair.width - 40, h, t), y=chair.back_y, z=chair.seat_h)34 # a side face's frame starts at z = 0, below the parent's origin, and its Y is up35 face = back.face("-Y").move(y=chair.seat_h + h * 0.6)36 buttons = s3d.at(s3d.dome("button", button_d, 5), on=s3d.grid(3, 2, 100, 110, on=face))37 s3d.attach(chair, "back", back, buttons)38 39 40def add_rails(chair, d=18, z=160):41 """A rail between the front and back legs, on each side."""42 # both=True: centred on its middle, so turned about X it spans the depth evenly43 rail = s3d.extrude("rail", s3d.circle(d), chair.depth - 2 * chair.inset, both=True)44 sides = s3d.grid(2, 1, chair.width - 2 * chair.inset, on=s3d.Frame((0, 0, z)))45 s3d.attach(chair, "rails", s3d.at(rail, rx=90, on=sides))46 47 48# ---------------------------------------------------------------- the parts49 50def seat_board(width, depth, t):51 return s3d.fillet(s3d.box("seat", width, depth, t), 8, edges="top")52 53 54def leg(h, foot=26, top=36):55 """Tapered, its foot on z = 0."""56 return s3d.cone("leg", foot, h, d2=top)57 58 59def back_panel(width, h, t):60 return s3d.fillet(s3d.box("back", width, t, h), 6, edges="top")
60 lines · 776 tokens (o200k_base)
1"""The weight stack of a cable machine.2 3A column of plates resting in two rubber bumpers, two guide rods standing over the4top plate, and the selector: two nuts and a stem on top, the pin's head at the5chosen plate and a spring lanyard hanging from under the nuts down to it.6 7Millimetres. The stack's frame has its origin under the middle of the lowest8plate, X along the plates, -Y towards the user.9"""10import json11import sys12 13import sand3d as s3d14 15 16# ---------------------------------------------------------------- the object: architecture first, then details17 18def weight_stack(n_plates=14, selected=9):19 """The plates, then each detail on their anchors. Any `add_` line can be deleted:20 the rest of the object does not change."""21 stack = plate_column(n_plates)22 add_bumpers(stack)23 add_guide_rods(stack)24 add_selector(stack, selected)25 return stack.root26 27 28def plate_column(n_plates=14, pitch=24, plate_h=22, plate_d=100, guide_spacing=300):29 """`n_plates` plates `pitch` apart. Publishes where details go: every plate's30 height, the top face, the front face and the two guide lines."""31 bottoms = [i * pitch for i in range(n_plates)]32 plate_z = [z + plate_h / 2 for z in bottoms]33 return s3d.assembly("weight_stack",34 *[s3d.at(weight_plate(d=plate_d, h=plate_h), z=z) for z in bottoms],35 plate_z=plate_z, top=bottoms[-1] + plate_h, front=-plate_d / 2,36 guides=(-guide_spacing / 2, guide_spacing / 2))37 38 39# ---------------------------------------------------------------- details, each on the stack's anchors40 41def add_bumpers(stack, sink=10):42 """Two rubber cups under the guide lines, taking the lowest plate `sink` deep."""43 s3d.attach(stack, "bumpers", *[s3d.at(bumper(), x=x, z=sink, anchor="top") for x in stack.guides])44 45 46def add_guide_rods(stack, gap=8):47 """Two rods on the guide lines, standing `gap` above the top plate."""48 s3d.attach(stack, "guide_rods", *[s3d.at(guide_rod(), x=x, z=stack.top + gap, anchor="bottom")49 for x in stack.guides])50 51 52def add_selector(stack, selected=9, nut_t=6, nut_gap=4, head_out=12, lanyard_y=-28):53 """On the top plate, two nuts and the stem; the lanyard's loop sits under the54 upper nut and its spring hangs in front, down to the pin head at plate `selected`."""55 upper = stack.top + nut_t + nut_gap # under the upper nut56 pinned = stack.plate_z[selected]57 s3d.attach(stack, "selector",58 s3d.at(hex_nut(t=nut_t), z=stack.top, anchor="bottom"),59 s3d.at(hex_nut(t=nut_t), z=upper, anchor="bottom"),60 s3d.at(selector_stem(), z=stack.top),61 s3d.at(pin_head(), y=stack.front - head_out, z=pinned, rx=90),62 s3d.at(spring_lanyard(length=upper - pinned), y=lanyard_y, z=(upper + pinned) / 2, rz=-90))63 64 65# ---------------------------------------------------------------- parts, in the order they are called66 67def weight_plate(w=420, d=100, h=22, hole_d=16, edge_r=2):68 """A steel plate with softened edges, drilled front to back for the selector pin."""69 plate = s3d.fillet(s3d.box("weight_plate", w, d, h), edge_r)70 return s3d.hole(plate, hole_d, on=plate.face("-Y").move(y=h / 2))71 72 73def bumper(d_foot=50, d_mouth=46, h=60, wall=2):74 """A rubber cup the stack rests in, open at the top."""75 cup = s3d.revolve("bumper", [(0, -h / 2), (d_foot / 2, -h / 2), (d_mouth / 2, h / 2), (0, h / 2)])76 return s3d.shell(cup, wall, open="top")77 78 79def guide_rod(d=25, length=900, collar_d=35, collar_h=20):80 """A round rod with a collar at its foot."""81 foot, head = -length / 2, length / 282 return s3d.revolve("guide_rod", [(0, foot), (collar_d / 2, foot), (collar_d / 2, foot + collar_h),83 (d / 2, foot + collar_h), (d / 2, head), (0, head)])84 85 86def hex_nut(across_corners=32, hole_d=16.5, t=6):87 return s3d.extrude("hex_nut", s3d.polygon(6, across_corners) - s3d.circle(hole_d), t, both=True)88 89 90def selector_stem(d=15, h=30, wall=1.5):91 """The top of the stem that threads the plates, an open sleeve."""92 return s3d.tube("selector_stem", d, d - 2 * wall, h)93 94 95def pin_head(d=36, t=3, boss_d=12, boss_t=6, hole_d=8):96 """The head of the selector pin, a disc with a boss, facing the user."""97 return s3d.revolve("pin_head", [(hole_d / 2, -boss_t / 2), (boss_d / 2, -boss_t / 2), (boss_d / 2, boss_t / 2 - t),98 (d / 2, boss_t / 2 - t), (d / 2, boss_t / 2), (hole_d / 2, boss_t / 2)])99 100 101def spring_lanyard(length=150, loop_d=44, loop_gap=40, neck=40, coil_x=24, lean=12, coil_d=4, wire_d=2.2,102 pitch=2.6):103 """A loop lying flat on top, round the selector stem and open by `loop_gap`104 degrees, and a close-wound spring hanging `length` down from it, `neck` away105 from the loop's centre and leaning `lean` outwards by its foot."""106 top = length / 2 - wire_d / 2107 loop = s3d.arc((coil_x - neck, 0, top), loop_d / 2, loop_gap / 2, 360 - loop_gap / 2)108 hanging = s3d.at(s3d.helix(coil_d, pitch, 2 * top), x=coil_x, z=top, rx=180) # from the loop, down109 coil = [(x + lean * (top - z) / (2 * top), y, z) for x, y, z in hanging.points]110 return s3d.wire("spring_lanyard", list(loop.points) + coil, wire_d)111 112 113if __name__ == "__main__":114 s3d.empty_scene()115 s3d.export(s3d.group("weight_stack", weight_stack(**json.loads(sys.argv[2] if len(sys.argv) > 2 else "{}"))),116 sys.argv[1] if len(sys.argv) > 1 else "weight_stack.glb")
116 lines · 1690 tokens (o200k_base)
1import sand3d as s3d2 3 4def lamp(stem_h=300):5 base = s3d.revolve("base", [(0, 0), (70, 0), (60, 20), (0, 20)])6 stem = s3d.at(s3d.cylinder("stem", 12, stem_h), on=base.face("top"))7 lamp = s3d.assembly("lamp", base, stem, head=s3d.Frame((0, 0, 20 + stem_h)))8 add_shade(lamp)9 return lamp10 11 12def add_shade(lamp, d=180):13 shade = s3d.shell(s3d.cone("shade", d, 120, d2=d / 2), 1.5, open="top bottom")14 s3d.attach(lamp, "shade", s3d.at(shade, on=lamp.head))
14 lines · 194 tokens (o200k_base)
1"""A spoon and a fork, from generic verbs — the regression test for "the vocabulary has a2hole": MeshCoder had to hard-code both.3 4Millimetres; the piece lies along +X from its business end, Z up. The handle is shared:5a loft of rounded sections along a gently rising line, narrow at the neck and widening6to a rounded end.7"""8import math9 10import sand3d as s3d11 12 13def handle(length=135, neck=(8.5, 2.6), end=(24, 3.2), rise=16, start=(0, 0, 0), stations=7, end_steps=6):14 """Sections from the neck to the end of the handle, square to its line; the last15 ones close it on a half circle in plan, so the end is round."""16 x0, y0, z0 = start17 r = end[0] / 218 body = length - r # where the round end begins19 20 def place(x, width, thick):21 u = (x - x0) / length22 return s3d.at(s3d.superellipse(width, thick, 4), x=x, y=y0, z=z0 + rise * (3 * u ** 2 - 2 * u ** 3), rz=90)23 sections = []24 for k in range(stations):25 u = k / (stations - 1)26 sections.append(place(x0 + body * u, neck[0] + (end[0] - neck[0]) * u ** 1.6,27 neck[1] + (end[1] - neck[1]) * u))28 for k in range(1, end_steps + 1):29 a = math.radians(85 * k / end_steps)30 sections.append(place(x0 + body + r * math.sin(a), end[0] * math.cos(a), end[1] * (1 - 0.3 * k / end_steps)))31 return s3d.loft("handle", *sections, follow=True)32 33 34def spoon(bowl=(62, 42), depth=13, wall=1.4, rim_r=0.5):35 """A bowl — half an ellipsoid, hollowed, its rim softened — and the handle."""36 length, width = bowl37 cup = s3d.scale(s3d.sphere("bowl", 1), (length, width, 2 * depth))38 s3d.split(cup, s3d.Frame((0, 0, depth)), keep="below")39 cup = s3d.at(cup, z=-depth) # its rim on z = 040 s3d.shell(cup, wall, open="cut")41 s3d.fillet(cup, rim_r, edges="shell")42 return s3d.join(cup, s3d.at(handle(start=(length / 2 - 3, 0, -wall / 2)), z=0))43 44 45def fork(head=(48, 25), tines=4, gap=2.2, root=17, t=2.2, bend_r=140):46 """A flat head cut into `tines` from its tip back to `root`, bent to a curve by47 keeping what a curved plate shares with it — and the handle."""48 length, width = head49 outline = s3d.poly([(-length, -width / 2), (0, -width / 2 + 3), (8, -4.5), (8, 4.5), (0, width / 2 - 3),50 (-length, width / 2)], fillet=[4, 0, 3, 3, 0, 4])51 pitch = width / tines52 for k in range(1, tines):53 y = -width / 2 + k * pitch54 outline = outline - s3d.at(s3d.slot(2 * (length - root) + gap, gap), x=-length, y=y)55 prism = s3d.extrude("head", outline, 60, both=True)56 steps = 2457 curve = [(bend_r * math.sin(a), 0, bend_r * (1 - math.cos(a))) # through the neck, rising to the tips58 for a in (-0.42 + 0.5 * k / steps for k in range(steps + 1))]59 plate = s3d.sweep("bend", s3d.rect(t, 4 * width), curve)60 s3d.intersect(prism, plate)61 return s3d.join(prism, handle(start=(6, 0, -t / 4)))
61 lines · 1024 tokens (o200k_base)
1"""A spoon and a fork, from generic verbs — the regression test for "the vocabulary has a2hole": MeshCoder had to hard-code both.3 4Millimetres; the piece lies along +X from its business end, Z up. The handle is shared:5a loft of rounded sections along a gently rising line, narrow at the neck and widening6to a rounded end.7"""8import math9 10import sand3d as s3d11 12 13def handle(length=135, neck=(8.5, 2.6), end=(24, 3.2), rise=16, start=(0, 0, 0), stations=7, end_steps=6):14 """Sections from the neck to the end of the handle, square to its line; the last15 ones close it on a half circle in plan, so the end is round."""16 x0, y0, z0 = start17 r = end[0] / 218 body = length - r # where the round end begins19 20 def place(x, width, thick):21 u = (x - x0) / length22 return s3d.at(s3d.superellipse(width, thick, 4), x=x, y=y0, z=z0 + rise * (3 * u ** 2 - 2 * u ** 3), rz=90)23 sections = []24 for k in range(stations):25 u = k / (stations - 1)26 sections.append(place(x0 + body * u, neck[0] + (end[0] - neck[0]) * u ** 1.6,27 neck[1] + (end[1] - neck[1]) * u))28 for k in range(1, end_steps + 1):29 a = math.radians(85 * k / end_steps)30 sections.append(place(x0 + body + r * math.sin(a), end[0] * math.cos(a), end[1] * (1 - 0.3 * k / end_steps)))31 return s3d.loft("handle", *sections, follow=True)32 33 34def spoon(bowl=(62, 42), depth=13, wall=1.4, rim_r=0.5):35 """A bowl — half an ellipsoid, hollowed, its rim softened — and the handle."""36 length, width = bowl37 cup = s3d.scale(s3d.sphere("bowl", 1), (length, width, 2 * depth))38 s3d.split(cup, s3d.Frame((0, 0, depth)), keep="below")39 cup = s3d.at(cup, z=-depth) # its rim on z = 040 s3d.shell(cup, wall, open="cut")41 s3d.fillet(cup, rim_r, edges="shell")42 return s3d.join(cup, s3d.at(handle(start=(length / 2 - 3, 0, -wall / 2)), z=0))43 44 45def fork(head=(48, 25), tines=4, gap=2.2, root=17, t=2.2, bend_r=140):46 """A flat head cut into `tines` from its tip back to `root`, bent to a curve by47 keeping what a curved plate shares with it — and the handle."""48 length, width = head49 outline = s3d.poly([(-length, -width / 2), (0, -width / 2 + 3), (8, -4.5), (8, 4.5), (0, width / 2 - 3),50 (-length, width / 2)], fillet=[4, 0, 3, 3, 0, 4])51 pitch = width / tines52 for k in range(1, tines):53 y = -width / 2 + k * pitch54 outline = outline - s3d.at(s3d.slot(2 * (length - root) + gap, gap), x=-length, y=y)55 prism = s3d.extrude("head", outline, 60, both=True)56 steps = 2457 curve = [(bend_r * math.sin(a), 0, bend_r * (1 - math.cos(a))) # through the neck, rising to the tips58 for a in (-0.42 + 0.5 * k / steps for k in range(steps + 1))]59 plate = s3d.sweep("bend", s3d.rect(t, 4 * width), curve)60 s3d.intersect(prism, plate)61 return s3d.join(prism, handle(start=(6, 0, -t / 4)))
61 lines · 1024 tokens (o200k_base)
1import sand3d as s3d2 3 4def rack(n=4, pitch=30, depth=40, t=5):5 base = s3d.extrude("base", s3d.rect(n * pitch, depth, r=3), t)6 pegs = s3d.at(s3d.cylinder("peg", 6, 20), on=s3d.grid(n, 1, pitch), z=t) # a copy per frame7 hook = s3d.at(s3d.extrude("hook", s3d.rect(4, 12), 12), x=n * pitch / 2 - 4, z=t)8 image = s3d.transform(s3d.copy(hook), mirror="x") # its image across x = 09 s3d.join(base, pegs, hook, image) # a list is its elements10 s3d.hole(base, 4, on=s3d.grid(2, 1, (n - 1) * pitch).move(y=depth / 2 - 6))11 s3d.chamfer(base, 0.6, edges="peg")12 return base
12 lines · 255 tokens (o200k_base)
1"""A front-loading washing machine, articulated: what #82's vocabulary writes in one piece.2 3Millimetres; the front looks +X, Z up. Four joints, each on a frame read on the cabinet:4the door centred on the porthole, hinged at its outer -Y edge so that it clears the5cabinet (it opens outward about "-Z", 0–143°), the drum endless about the front's6normal, the detergent drawer sliding out 0–200 mm, the dial endless. The drawer's bay and the dial's seat are cut by `seat`, as they sit at rest;7the door's glass is seated in a groove of its ring. Built at rest; `washer_open()` poses it.8"""9import sand3d as s3d10 11 12def washer(w=600, d=600, h=850, porthole=440, door_d=480, drum_d=420):13 cab = s3d.box("cabinet", w, d, h)14 s3d.fillet(cab, 12, edges="side")15 front = cab.face("+X") # X is +Y, Y is up16 s3d.pocket(cab, s3d.circle(porthole), d - 100, on=front.move(0, 400))17 s3d.material(cab, "plastic", color="#f2f2f0")18 19 back = front.move(0, 400).offset(-(d - 200)) # a solid's height runs along its frame's normal20 drum = s3d.at(s3d.cylinder("drum", drum_d, d - 220), on=back)21 s3d.cut(drum, s3d.at(s3d.cylinder("bore", drum_d - 20, d - 229), on=back.offset(10)))22 s3d.join(drum, s3d.at(s3d.cylinder("axle", 40, 60), on=back.offset(-60)))23 s3d.material(drum, "brushed_steel")24 25 ring = s3d.at(s3d.tube("ring", door_d, 300, 45), on=front.move(0, 400)) # centred on the porthole26 glass = s3d.at(s3d.cylinder("glass", 320, 16), on=front.move(0, 400).offset(14))27 hinge = front.move(-door_d / 2, 400).offset(45) # the ring's outer -Y edge: it clears the cabinet28 s3d.seat(ring, glass)29 s3d.material(ring, "painted_metal", color="#3a3d42")30 s3d.material(glass, "glass", color="#cfe3e6", opacity=0.4)31 handle = s3d.at(s3d.box("handle", 24, 110, 30), on=s3d.bbox(ring, "+X").move(x=door_d / 2 - 40))32 door = s3d.group("door", ring, glass, handle) # the handle and the glass move with it33 34 top_band = front.move(0, h - 75)35 drawer = s3d.at(s3d.box("drawer", 200, 100, 231), on=top_band.move(150).offset(-230))36 s3d.join(drawer, s3d.at(s3d.box("drawer_front", 220, 120, 15), on=top_band.move(150)))37 s3d.seat(cab, drawer)38 dial = s3d.at(s3d.cylinder("dial", 60, 30), on=top_band.move(-150).offset(-10))39 s3d.seat(cab, dial)40 s3d.material([drawer, dial], "plastic", color="#e6e6e3")41 42 s3d.joint("door_hinge", door, hinge, axis="-Z", range=(0, 143))43 s3d.joint("drum_spin", drum, front.move(0, 400), kind="continuous")44 s3d.joint("drawer_slide", drawer, top_band.move(150), kind="prismatic", axis="+X", range=(0, 200))45 s3d.joint("dial_turn", dial, top_band.move(-150), kind="continuous")46 return s3d.assembly("washing_machine", cab, drum, door, drawer, dial, front=front)47 48 49def washer_open():50 machine = washer()51 s3d.pose(machine, 92, joint="door_hinge")52 s3d.pose(machine, 120, joint="drawer_slide")53 s3d.pose(machine, 30, joint="dial_turn")54 return machine
54 lines · 1038 tokens (o200k_base)
1import sand3d as s3d2 3 4def gate(w=750, h=1200, post=100):5 l_post = s3d.at(s3d.box("post", post, post, h + 300), x=-w - post / 2)6 r_post = s3d.transform(s3d.copy(l_post), mirror="x")7 board = s3d.at(s3d.box("board", w, 40, h), x=-w / 2, z=150) # from the post to x = 08 handle = s3d.at(s3d.cylinder("handle", 20, 60), on=s3d.bbox(board, "+Y").move(x=60 - w / 2)) # near x = 09 leaf = s3d.group("leaf", board, handle) # the handle moves with it10 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 +Y11 twin = s3d.transform(s3d.copy(leaf), mirror="x") # its image, hinged at x = +w: opens toward +Y too12 gate = s3d.assembly("gate", l_post, r_post, leaf, twin)13 s3d.pose(leaf, 60)14 return gate
14 lines · 315 tokens (o200k_base)
1import sand3d as s3d2 3 4def bracket(w=80, d=40, t=6, hole_d=6.6, pitch=50):5 plate = s3d.extrude("plate", s3d.rect(w, d, r=4), t)6 s3d.hole(plate, hole_d, on=s3d.grid(2, 1, pitch, 0))7 s3d.fillet(plate, 1, edges="top")8 return plate
8 lines · 107 tokens (o200k_base)
1"""f29 — a tube standing at one end of a plate whose other end is square, a rib along the2plate's middle, cut back on a slant.3 4Source: CADFS `00638437` (Onshape FeatureScript, CC BY 4.0). One Top-plane5sketch gives the tube (the ring between its two circles) and the plate (the rectangle6outside the outer circle). The rib's slant is drawn on its -Y side as a diagonal; the7region cut through is the face's other side of it, above the diagonal: the rib becomes8a gusset rising to the tube.9"""10import sand3d as s3d11 12 13def build(half_l=58.34907, half_w=38.61544, bore_r=25.40422, tube_h=82.55, plate_t=25.4, rib=(72.80118, 9.58033),14 rib_h=53.848, slant_x=(-36.40059, 20.03189)):15 tube_at = dict(x=half_l)16 ring = s3d.at(s3d.circle(2 * half_w), **tube_at) - s3d.at(s3d.circle(2 * bore_r), **tube_at)17 part = s3d.extrude("tube", ring, tube_h)18 plate = s3d.rect(2 * half_l, 2 * half_w) - s3d.at(s3d.circle(2 * half_w), **tube_at)19 s3d.join(part, s3d.extrude("plate", plate, plate_t), s3d.extrude("rib", s3d.rect(*rib), rib_h))20 x0, x1 = slant_x21 side = s3d.Frame((0, -rib[1] / 2, 0), (0, -1, 0))22 over = 5 # past the rib's top and end, so the cut leaves no skin23 above = s3d.poly([(x0, plate_t), (x1, rib_h), (x1, rib_h + over), (x0 - over, rib_h + over), (x0 - over, plate_t)])24 return s3d.cut(part, s3d.extrude("slant", above, -plate_t, on=side))
24 lines · 506 tokens (o200k_base)
1"""f28 — a turned part: a flanged base and a tall thin sleeve rising from it.2 3Source: CADFS `00577926` (Onshape FeatureScript, CC BY 4.0). One outline4on the Right plane, turned a full revolution about its own edge E9, which lies on the5Z axis: sketch X is the radius, sketch Y the height.6"""7import sand3d as s3d8 9 10def build(base_r=55.19564, base_z=-11.60857, step_r=44.87869, shoulder_z=10.05700, sleeve=(3.61093, 9.80110),11 sleeve_top=55.45154):12 r_in, r_out = sleeve13 return s3d.revolve("turned", [(0, base_z), (base_r, base_z), (base_r, 0), (step_r, 0), (step_r, shoulder_z),14 (r_out, shoulder_z), (r_out, sleeve_top), (r_in, sleeve_top), (r_in, shoulder_z),15 (0, shoulder_z)])
15 lines · 236 tokens (o200k_base)
1"""f06 — a large turned body: one outline revolved about a line of its own.2 3Source: CADFS `00421068` (Onshape FeatureScript, CC BY 4.0). A first4revolve is made and deleted, as in the source; the second outline, drawn on the5Front plane, turns a full revolution about its edge E48 (along X, 115.78 below the6origin).7"""8import math9 10import sand3d as s3d11 12FRONT = s3d.Frame((0, 0, 0), (0, -1, 0))13 14 15def revolve_about(name, outline, a, b):16 """`outline` (Front-plane points) turned a full revolution about the line a → b."""17 ax, ay = a18 dx, dy = b[0] - ax, b[1] - ay19 n = math.hypot(dx, dy)20 ux, uy = dx / n, dy / n21 section = [(abs((x - ax) * uy - (y - ay) * ux), (x - ax) * ux + (y - ay) * uy) for x, y in outline]22 axis = s3d.Frame(FRONT.point(ax, ay), FRONT.point(ux, uy) - FRONT.point())23 return s3d.at(s3d.revolve(name, section), on=axis)24 25 26FIRST = [(-30.605, 50.28), (-5.538, 50.28), (-5.538, 46.49), (9.910, 49.405), (36.726, 45.324), (36.726, 29.876),27 (22.152, 11.222), (18.071, -13.554), (31.188, -23.464), (11.368, -29.876), (2.332, -12.096),28 (-20.986, -20.258), (-29.439, -6.850), (-29.439, 11.513), (-37.600, 25.213), (-37.600, 39.786),29 (-62.376, 46.490), (-62.376, 55.235), (-67.331, 55.235), (-67.331, 61.356), (-56.255, 63.687),30 (-54.065, 53.287), (-46.045, 54.976)]31BODY = [(-65.44221, 49.85064), (-49.00322, 49.85064), (-49.00322, 34.71444), (-20.88390, 34.71444), (0, 9.63158),32 (32.32650, 9.63158), (40.97859, 23.03794), (82.07605, 40.33646), (80.13092, 44.95765), (91.30851, 49.66246),33 (95.23396, 40.33646), (111.92579, 40.33646), (115.38663, 60.66222), (127.01517, 58.68225),34 (127.01517, 49.85064), (119.28008, 49.85064), (115.38663, 31.68720), (56.11977, 19.14577),35 (28.00045, -6.80201), (4.63978, -13.28896), (4.63978, -68.64423), (16.75272, -68.64423),36 (23.24179, -79.02335), (34.05691, -79.02335), (34.92212, -85.07783), (34.92212, -115.78270),37 (15.88751, -115.78270), (15.88751, -87.24014), (0, -87.24014), (0, -10.26172), (-13.52962, -6.80201),38 (-13.52962, 6.60434), (-33.42944, 23.47040), (-60.25095, 23.47040), (-60.25095, 40.33646),39 (-65.44220, 40.33646)]40 41 42def build():43 s3d.delete(revolve_about("first", FIRST, (31.188, -23.464), (11.368, -29.876)))44 return revolve_about("body", BODY, (34.92212, -115.78270), (15.88751, -115.78270))
44 lines · 1040 tokens (o200k_base)
1"""f03 — a ring notched round its outside, the notches only through its lower half,2their four vertical edges chamfered.3 4Source: Zero-To-CAD-1m `0d84288b-5014-5b32-4355-9452883f472d` (CadQuery, Apache-2.0).5The source's tooth cutters are boxes centred on Z = 0, so they cut the lower half of6a ring that stands on Z = 0; they are merged into one tool first, as the source makes7a compound of them. Its coolant holes fall inside the bore and cut nothing: left out,8since 1.0 a cut that touches nothing raises (#52).9The chamfers — on the notch mouths and, filling, in the notch corners — are drawn10into the cutter's outline.11"""12import math13 14import sand3d as s3d15 16 17def build(outer_d=80, rim=12, thickness=10, teeth=20, notch_depth=9, notch_share=0.6, notch_h=5, chamfer=0.8):18 r = outer_d / 219 ring = s3d.extrude("ring", s3d.circle(outer_d) - s3d.circle(outer_d - 2 * rim), thickness)20 half = math.pi * (outer_d - notch_depth) / teeth * notch_share / 221 corner = math.sqrt(r * r - half * half) # where a notch wall meets the rim22 lip = math.atan2(half, corner) + chamfer / r # where its chamfer meets the rim23 start = (corner - chamfer, half)24 flare = (start[0] + 3 * (r * math.cos(lip) - start[0]), start[1] + 3 * (r * math.sin(lip) - start[1]))25 notch = s3d.poly([(r - notch_depth, -half), (start[0], -half), (flare[0], -flare[1]), flare, start,26 (r - notch_depth, half)], chamfer=[chamfer, 0, 0, 0, 0, chamfer])27 s3d.cut(ring, s3d.merge(*[s3d.extrude("notch", s3d.at(notch, rz=360 * k / teeth), notch_h, h2=1)28 for k in range(teeth)]))29 return ring
29 lines · 534 tokens (o200k_base)
1"""f11 — a square frame with a rounded window, two holes on its middle line, four2drilled holes at its corners and a blind hole driven into its -Y side along its top3edge.4 5Source: CADFS `00305103` (Onshape FeatureScript, CC BY 4.0). The side6hole is placed at (0, 6.35) on the -Y face's plane — on the top edge — and goes714.4 deep, through the frame's bar into the window: half a hole, a groove.8"""9import sand3d as s3d10 11 12def build(outer=70, window=50, window_r=10, t=6.35, side_d=6, side_x=30, side_hole=(5, 14.4),13 corner_d=5, corner_pitch=60, corner_depth=15):14 shape = (s3d.rect(outer, outer) - s3d.rect(window, window, r=window_r)15 - s3d.at(s3d.circle(side_d), x=-side_x) - s3d.at(s3d.circle(side_d), x=side_x))16 plate = s3d.extrude("plate", shape, t)17 d, depth = side_hole18 s3d.hole(plate, d, on=plate.face("-Y").move(0, t), depth=depth)19 s3d.hole(plate, corner_d, on=s3d.grid(2, 2, corner_pitch, corner_pitch, on=plate.face("top")), depth=corner_depth)20 return plate
20 lines · 345 tokens (o200k_base)
1"""f25 — a tall cover lofted through six rectangles that narrow and turn as they rise,2rib grooves cut into its foot, its top and bottom edges rounded.3 4Source: Zero-To-CAD-1m `5ad9d45b-ea4f-8079-1186-76e3b8d6a1b1` (CadQuery, Apache-2.0).5Each `workplane(offset=)` and `transformed(rotate=)` in the loop is relative to the6last — a frame offset and turned from the one before: the sections stand at 0, 16,748, 96, 160, 240 and turn 0, 12, 36, 72, 120, 180 degrees. The blind holes are extruded8below Z = 0, under the cover: left out, they cut nothing and since 1.0 a cut that touches9nothing raises (#52).10"""11import sand3d as s3d12 13 14def build(length=100, height=80, t=5, steps=5, start_w=60, end_w=30, turn=12, rib_t=2, rib_spacing=12, edge_r=1.5):15 step = height / steps16 plane = s3d.Frame()17 sections = [s3d.rect(start_w, length)]18 for i in range(1, steps + 1):19 plane = plane.offset(i * step).turn(rz=i * turn)20 sections.append(s3d.at(s3d.rect(start_w - (start_w - end_w) * i / steps, length), on=plane))21 cover = s3d.loft("cover", *sections)22 rib_h = t - 0.823 x = -(start_w / 2 - rib_spacing)24 s3d.cut(cover, *[s3d.at(s3d.box("rib", rib_t, length, rib_h), x=x, y=i * rib_spacing, z=t / 2)25 for i in range(int(height / rib_spacing))])26 return s3d.fillet(cover, edge_r, edges="start end")
26 lines · 458 tokens (o200k_base)
1"""f26 — a wall flange: a coned cup for a hub, a flange round it, a hole through the2cup's point, its top edges chamfered and a ring of small knurls standing on its rim.3 4Source: Zero-To-CAD-1m `8719a024-46f7-2edf-2d5b-96f11059d3db` (CadQuery, Apache-2.0).5The revolved outline closes on a diagonal from (20, 20) to the origin: the hub is a6cone-shaped cup. `faces('>X').first()` picks the flat top ring, so the knurls stand7on it; the countersunk hole the source's helper drills there coincides with the8cup's own cone and removes nothing — it is left out.9"""10import sand3d as s3d11 12 13def build(hub_r=20, hub_h=12, flange_r=45, flange_t=8, chamfer=0.5, bore=10, knurls=24, knurl=(2, 1.5), knurl_h=1):14 top = hub_h + flange_t15 body = s3d.revolve("flange", [(0, 0), (hub_r, 0), (hub_r, hub_h), (flange_r, hub_h), (flange_r, top),16 (hub_r, top)])17 s3d.chamfer(body, chamfer, edges="top")18 s3d.hole(body, bore, on=body.face("top"))19 w, h = knurl20 return s3d.join(body, *[s3d.extrude("knurl", s3d.rect(w, h), knurl_h, on=f)21 for f in s3d.ring(knurls, flange_r - knurl_h / 2, on=body.face("top"))])
21 lines · 406 tokens (o200k_base)
1"""f14 — an elliptical plate, its top edge rounded right through its thickness, the word2"Rato" standing on it.3 4Source: CADFS `00466650` (Onshape FeatureScript, CC BY 4.0). The text is5NotoSans Bold (fixtures/fonts, SIL OFL), drawn on the Front plane and extruded 10 mm6symmetrically. The source gives it no box: Onshape's default size and place are read7off the reference — its ink 107.4 mm wide, topping out at Z = 45.1, centred on X 0.6.8"""9from pathlib import Path10 11import sand3d as s3d12 13FONT = Path(__file__).resolve().parent / "fonts/NotoSans-Bold.ttf"14FRONT = s3d.Frame((0, 0, 0), (0, -1, 0))15 16 17def build(major=70.9123, minor=34.8954, t=10, edge_r=10, word="Rato", ink_w=107.39, ink_top=45.125, ink_x=0.615):18 plate = s3d.extrude("plate", s3d.ellipse(2 * major, 2 * minor), t)19 s3d.fillet(plate, edge_r, edges="top")20 letters = s3d.text(word, 1, font=FONT)21 (x0, y0), (x1, y1) = letters.bounds()22 size = ink_w / (x1 - x0)23 letters = s3d.text(word, size, font=FONT)24 (x0, y0), (x1, y1) = letters.bounds()25 sign = s3d.extrude("text", s3d.at(letters, x=ink_x, y=ink_top - y1), t, both=True, on=FRONT)26 return s3d.join(plate, sign)
26 lines · 421 tokens (o200k_base)
A program is plain Python: import sand3d as s3d, then verbs. Makers
take a name and return a solid; edits change it and return it.
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
9 lines · 246 tokens (o200k_base)
at returns the copies.transform(…, mirror="x").join takes the list of pegs as it is, then two holes and a chamfer on the edges named after the pegs.Learn these 6 and the rest of the API reads by itself.
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).face("top") starts at x = 0. Build at the origin, read faces, then place; or read
bbox(ob, "+Z"), the centre of the side wherever the part is: the frame to hinge on. And
join fuses parts that don't touch without a word: they end up as separate pieces of one object.A profile is a flat outline in XY. It is not a solid: it becomes one through extrude, revolve, sweep or loft. Profiles add and subtract with + and -.
rect(w, d, r=0, chamfer=0)Centred; corners rounded or cutcircle(d)Centredellipse(w, d)Centredslot(l, d)Round-ended slot along Xpolygon(n, d)Regular polygon, corner on +XEvery basic shape is centred on its origin. rect takes r= for rounded corners and chamfer= for cut ones.
shapes = [s3d.rect(40, 24, r=5), s3d.rect(40, 24, chamfer=6), s3d.circle(30), s3d.ellipse(40, 20), s3d.slot(44, 16), s3d.polygon(6, 32)] for i, shape in enumerate(shapes): s3d.extrude("plate", s3d.at(shape, x=50 * i), 3)
4 lines · 107 tokens (o200k_base)
poly(pts, fillet=0, chamfer=0)Outline through points, arcs, splinesarc(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 runtangent_arc(to)Poly run: arc leaving tangentspline(pts, closed=False, tangents=None, loop=False)Smooth curve; closed: a profile; loop: a ring pathpoly joins points with straight lines. fillet= and chamfer= take one value per corner. Inside a poly, arc(through=, to=) and tangent_arc(to) bend the run. A closed spline is a smooth profile.
bracket = s3d.poly([(0, 0), (40, 0), (40, 10), (15, 10), (15, 30), (0, 30)], fillet=[0, 0, 0, 5, 0, 0]) hook = s3d.poly([(0, 0), (30, 0), s3d.arc(through=(40, 12), to=(30, 24)), (10, 24), s3d.tangent_arc((10, 40)), (0, 40)]) blob = s3d.spline([(0, 0), (20, 5), (30, 20), (10, 30), (-5, 15)], closed=True) s3d.extrude("bracket", bracket, 3) s3d.extrude("hook", s3d.at(hook, x=55), 3) s3d.extrude("blob", s3d.at(blob, x=110, y=5), 3)
7 lines · 229 tokens (o200k_base)
offset(p, t)Grown by tmirror(p, flip='x')Plus its image, flip negatedsuperellipse(w, d, n=2, taper=0)Ellipse (n=2) to box (n=4); taper narrows its +Y side, an eggtext(s, size, font=None)Centred outlineoffset grows a profile by a distance (shrinks it if negative). mirror(p) adds the image of p: flip="x" negates X. superellipse goes from an ellipse (n = 2) to a rounded box (n = 4 and more).
frame = s3d.offset(s3d.rect(30, 20), 5) - s3d.rect(30, 20) wing = s3d.mirror(s3d.poly([(0, 0), (25, 0), (25, 6), (6, 22), (0, 22)])) s3d.extrude("frame", frame, 3) s3d.extrude("wing", s3d.at(wing, x=60), 3) s3d.extrude("pad", s3d.at(s3d.superellipse(40, 30, 4), x=115), 3) s3d.extrude("label", s3d.at(s3d.text("sand3d", 14), x=55, y=-35), 2)
6 lines · 179 tokens (o200k_base)
stroke(path, t)A band t wide along an open path, centred on it: a closed outlinewrap(*profiles)Convex outline round profilesstroke(path, t) thickens an open path into a band t wide, centred on it: a bent bracket, a hook. wrap is the convex outline round profiles: a belt round its wheels.
bracket = s3d.stroke([(0, 0), (50, 0), (50, 50)], 4) hook = s3d.stroke([(0, 0), (40, 0), s3d.tangent_arc((60, 20)), (60, 60)], 6) s3d.extrude("bracket", bracket, 4) s3d.extrude("hook", s3d.at(hook, x=70), 4) wheels = [s3d.at(s3d.circle(d), x=x, y=y) for x, y, d in [(170, 20, 50), (260, 20, 50), (215, 55, 20)]] belt = s3d.wrap(wheels) s3d.extrude("belt", s3d.offset(belt, 4) - belt, 10) for wheel in wheels: s3d.extrude("wheel", wheel, 10)
9 lines · 214 tokens (o200k_base)
A maker takes a name and returns a solid. Every solid starts at z = 0 and goes up, centred in X and Y.
box(name, w, d, h)On Z = 0, centred in X and Ycylinder(name, d, h)Along Z, from Z = 0tube(name, d, d_in, h)Along Z, from Z = 0cone(name, d, h, d2=0)Along Z, from Z = 0sphere(name, d)On Z = 0wedge(name, w, d, h, top=0)Box narrowed on toptorus(name, d, d_wire)Ring about Z, on Z = 0dome(name, d, h)Cap on Z = 0All of them stand on z = 0. cone(d, h, d2) goes from diameter d at the bottom to d2 at the top. torus(d, d_wire): d is the diameter of the wire's centre line. at(parts, on=grid) would put every part on every frame; zip pairs them one to one.
parts = [s3d.box("box", 30, 20, 20), s3d.cylinder("cylinder", 20, 30), s3d.tube("tube", 30, 20, 10), s3d.cone("cone", 30, 40, d2=10), s3d.sphere("sphere", 30), s3d.wedge("wedge", 40, 20, 30, top=10), s3d.torus("torus", 40, 8), s3d.dome("dome", 30, 10)] for part, place in zip(parts, s3d.grid(4, 2, 50, 60)): # one part per frame s3d.at(part, on=place)
5 lines · 177 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 onA prism up Z. taper narrows it as it rises, twist turns it, both=True centres it on its plane. on= builds it on a face, until= runs it up to a plane or a solid instead of a height.
plate = s3d.extrude("plate", s3d.rect(60, 40, r=4) - s3d.circle(16), 8) s3d.extrude("boss", s3d.circle(12), 10, on=plate.face("top").move(20, 10)) s3d.at(s3d.extrude("taper", s3d.rect(30, 30), 25, taper=10), x=-55) s3d.at(s3d.extrude("twist", s3d.rect(30, 14), 40, twist=90), x=55)
4 lines · 139 tokens (o200k_base)
revolve(name, profile, angle=360)X radius, Y height, turned about ZThe profile is drawn with X as the radius and Y as the height, then turned about Z. A list of points is a profile too. angle= turns it less than a full turn.
s3d.revolve("cup", [(0, 0), (20, 0), (24, 40), (22, 40), (18, 3), (0, 3)]) s3d.at(s3d.revolve("half", s3d.at(s3d.rect(10, 20), x=10, y=10), 180), x=60)
2 lines · 85 tokens (o200k_base)
sweep(name, profile, path, scale=1, twist=0, frenet=False)Profile along a pathloft(name, *sections, follow=False, loop=False)Through placed sections, capped; loop: a ringsweep carries a profile along a path, square to it. loft skins placed sections: follow=True turns each section with the path, loop=True closes the ring.
s3d.sweep("bar", s3d.rect(10, 4), [(0, 0, 0), (0, 0, 50), (50, 0, 50)]) s3d.at(s3d.sweep("horn", s3d.circle(10), s3d.arc((0, 0, 0), 40, 0, 90), scale=0.3, twist=30), x=-70) vase = [s3d.circle(40), s3d.at(s3d.circle(60), z=50), s3d.at(s3d.polygon(6, 30), z=100)] s3d.at(s3d.loft("vase", *vase), x=90)
4 lines · 166 tokens (o200k_base)
station(z, w, d, x=0, y=0, n=2, taper=0)Loft section at height z; n and taper as superellipsestation(z, w, d, x, y, n, taper) is a superellipse section at height z, centred at (x, y): a loft is then a table of rows. n goes from an ellipse (2) to a rounded box (4), taper narrows the +Y side into an egg.
rows = [(0, 300, 900, 50, 0.2), (40, 700, 1500, 0, 0.35), (90, 500, 1200, 20, 0)] s3d.loft("cabin", *[s3d.station(z, w, d, y=y, n=2.5, taper=t) for z, w, d, y, t in rows])
2 lines · 99 tokens (o200k_base)
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 sectionsbody(name, plan, top, bottom, n) builds along Y from the views a drawing gives: the plan's width, and the side view's top and bottom, as (y, value) points, splines or numbers. Each section is a superellipse; n may vary along Y.
nose_to_tail = [(-3500, 0), (-2500, 1000), (0, 1400), (3000, 900), (4500, 400)] top = [(-3500, 900), (-2500, 1400), (0, 1600), (3000, 1300), (4500, 1100)] bottom = [(-3500, 900), (-2500, 500), (0, 300), (3000, 600), (4500, 850)] s3d.body("fuselage", s3d.spline(nose_to_tail), s3d.spline(top), s3d.spline(bottom), n=[(-3500, 2), (0, 3), (4500, 2)]) hull = s3d.body("hull", [(0, 0), (600, 900), (2400, 1000), (3000, 840)], 800, [(0, 800), (600, 120), (3000, 200)], n=4) s3d.at(hull, x=3500)
8 lines · 252 tokens (o200k_base)
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 leasthelix(d, pitch, h, left=False)Coil path up Zwire is a round wire of diameter d along any path; fillet= rounds the corners of a path of points. helix(d, pitch, h) is a coil path up Z.
s3d.wire("hoop", s3d.arc((0, 0, 0), 30, 0, 270), 4) s3d.at(s3d.wire("spring", s3d.helix(30, 8, 40), 3), x=70) s3d.at(s3d.wire("rod", [(0, 0, 0), (0, 0, 30), (20, 0, 45)], 5, fillet=8), x=130)
3 lines · 119 tokens (o200k_base)
An edit changes its first argument in place and returns it. Edges and faces are reached by the names the maker gave them: top, bottom, side, rim, cut, the tool's name, or plate.top.
cut(ob, *tools)Subtract toolsjoin(ob, *others)Fuse others in; ob's material wins: group keeps each its ownintersect(ob, tool)Keep the common partThe tools are used up. A cut that touches nothing raises: it is a mistake, not a no-op.
plate = s3d.box("plate", 60, 40, 8) s3d.cut(plate, s3d.at(s3d.cylinder("pin", 10, 20), x=10, z=-6), s3d.at(s3d.box("notch", 10, 50, 20), x=-30, z=-2)) s3d.join(plate, s3d.at(s3d.cylinder("boss", 16, 10), x=-10, z=7)) s3d.at(s3d.intersect(s3d.box("cube", 30, 30, 30), s3d.at(s3d.sphere("ball", 40), z=-5)), x=70)
5 lines · 162 tokens (o200k_base)
hole(ob, d, on=(0, 0), depth=None, cbore=None, csk=None)On frames or points: through, or depth deeppocket(ob, profile, depth=None, on=None)Profile cut in from a facehole drills on points (x, y), on a frame or on a list of frames: through, or depth deep, with cbore=(d, depth) or csk=(d, angle). It starts where its axis enters the part. pocket cuts a profile in from a face.
plate = s3d.box("plate", 80, 50, 10) s3d.hole(plate, 6.6, on=[(-30, -15), (30, -15), (-30, 15), (30, 15)]) s3d.hole(plate, 5, on=(0, 12), cbore=(10, 4)) s3d.hole(plate, 5, on=(0, -12), csk=(11, 90)) s3d.pocket(plate, s3d.rect(14, 8, r=2), depth=4, on=plate.face("top").move(-20, 0)) s3d.pocket(plate, s3d.circle(8), depth=3, on=s3d.grid(1, 2, 0, 16, on=plate.face("top").move(20, 0)))
6 lines · 199 tokens (o200k_base)
fillet(ob, r, edges='all')Round named edgeschamfer(ob, d, edges='all', d2=None, angle=None)Bevel named edgessoften(ob, r, angle=30)Round all sharp edgesfillet rounds named edges to a radius; chamfer cuts them, d2= or angle= for an unequal cut; soften rounds every sharp edge.
s3d.fillet(s3d.box("a", 40, 30, 20), 4, edges="top") plate = s3d.box("p", 50, 40, 10) s3d.hole(plate, 14, on=(0, 0)) s3d.at(s3d.chamfer(plate, 2, edges="hole"), x=55) s3d.chamfer(s3d.at(s3d.box("b", 40, 30, 20), x=110), 3, edges="top", d2=6) s3d.soften(s3d.at(s3d.box("s", 40, 30, 20), x=165), 3)
6 lines · 162 tokens (o200k_base)
shell(ob, t, open=None)Hollow; open named faces, or the flat faces looking "+Y", "-X +X"split(ob, frame, keep='above')By a frame's planedent(ob, points, r, depth)Press hollows at pointsshell hollows a solid, open= names the faces it removes. split cuts by a frame's plane and keeps above, below or both. dent presses hollows at points, as buttons do in upholstery.
s3d.shell(s3d.fillet(s3d.box("tray", 40, 30, 20), 4, edges="bottom side"), 1.5, open="top") ball = s3d.split(s3d.sphere("ball", 40), s3d.Frame((0, 0, 25), (0, 1, 1))) s3d.at(ball, x=60) pad = s3d.extrude("pad", s3d.superellipse(60, 60, 4), 20) s3d.dent(pad, [(x, y, 20) for x in (-14, 14) for y in (-14, 14)], 9, 4) s3d.at(pad, x=130)
6 lines · 170 tokens (o200k_base)
copy(ob)An independent copy, its joints with itmerge(*obs)No booleanscale(ob, s)About its origindelete(ob)Remove itcopy is an independent copy that keeps its name. merge puts solids in one object without a boolean. scale scales the mesh about its origin. delete removes an object.
peg = s3d.cylinder("peg", 8, 20) s3d.at(s3d.copy(peg), x=20) s3d.merge(s3d.at(s3d.box("m", 10, 10, 10), x=50), s3d.at(s3d.box("n", 10, 10, 10), x=64)) s3d.at(s3d.scale(s3d.sphere("egg", 20), (1.6, 1, 1)), x=100) s3d.delete(s3d.at(s3d.box("gone", 10, 10, 10), x=140))
5 lines · 145 tokens (o200k_base)
A frame is a place to build on: an origin, a normal (its Z) and an X. Faces, grids, rings and paths give frames; at puts things on them.
Frame(origin=(0, 0, 0), normal=(0, 0, 1), up=None) · .offset(d) .turn(rx, ry, rz) .move(x, y, z) · ob.face(which)A place to build onob.face("top") is the frame of a named face, ob.face("+X") the face furthest that way. A frame moves along its own axes (.move), turns (.turn) and offsets along its normal (.offset).
plate = s3d.box("plate", 60, 40, 8) s3d.extrude("boss", s3d.circle(20), 10, on=plate.face("top").move(15, 5)) s3d.extrude("tab", s3d.rect(12, 6), 6, on=plate.face("+X").move(y=4)) # on a side, Y is up s3d.extrude("fin", s3d.rect(30, 3), 12, on=plate.face("top").turn(rz=30).move(-15))
4 lines · 134 tokens (o200k_base)
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"…at sets the values it is given and keeps the others. on= changes the frame it is placed in. anchor= picks the point of the bounding box that goes there: "top", "bottom-front-left"…
plate = s3d.box("plate", 60, 40, 8) post = s3d.at(s3d.cylinder("post", 10, 30), on=plate.face("top")) # stands on the top s3d.at(post, x=20) # moved along, still on the top s3d.at(s3d.box("b", 10, 20, 30), x=-40, rz=30, anchor="bottom-front-left")
4 lines · 106 tokens (o200k_base)
grid(nx, ny=1, px=0, py=0, stagger=False, on=None)On a gridring(n, r, start=0, angle=360, on=None)On a circleGiven a list of frames, at, hole and pocket act once per frame: at returns one copy per frame and uses the original up. grid and ring lay frames on a plane, the origin's or a face's.
disc = s3d.cylinder("disc", 80, 6) s3d.cut(disc, s3d.at(s3d.box("slot", 12, 3, 10), x=25, z=-2, on=s3d.ring(12, 0))) s3d.hole(disc, 4, on=s3d.ring(6, 12, on=disc.face("top"))) plate = s3d.box("plate", 70, 50, 4) s3d.join(plate, s3d.at(s3d.cylinder("stud", 8, 6), on=s3d.grid(4, 3, 15, 13, on=plate.face("top")))) s3d.at(plate, x=100) # built at the origin, placed last
6 lines · 181 tokens (o200k_base)
along(path, pitch=None, count=None)Along a pathsnap(targets, points, direction=None)On a surfacealong spaces frames on a path, by pitch or count. snap drops points onto a surface and returns frames that lie on it, facing out.
path = s3d.arc((0, 0, 0), 30, 0, 270) s3d.wire("track", path, 3) s3d.at(s3d.box("bead", 6, 6, 6), on=s3d.along(path, count=7)) body = s3d.sphere("body", 60) spots = s3d.snap(body, [(x, y, 70) for x in (-15, 0, 15) for y in (-15, 0, 15)]) rivets = s3d.at(s3d.dome("rivet", 6, 2), on=spots) s3d.transform([body, rivets], x=100) # moved together, as one
7 lines · 170 tokens (o200k_base)
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 closesob.edge("top") is the path along the edges a verb named: wire, sweep and along take it. A bead runs round the notch a cut left; a moulding and studs follow a plate's top.
cyl = s3d.cylinder("cyl", 40, 60) s3d.cut(cyl, s3d.at(s3d.box("notch", 20, 20, 20), x=20, z=20)) s3d.wire("bead", cyl.edge("notch"), 3) # round the loop the cut left plate = s3d.at(s3d.box("plate", 80, 40, 6), x=100) top = plate.edge("top") s3d.sweep("trim", s3d.rect(2, 3), top) # a moulding round the top s3d.at(s3d.sphere("stud", 4), on=s3d.along(top, pitch=20))
7 lines · 170 tokens (o200k_base)
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 meshtransform moves an object from where it is, as a parent would: a door opens on its hinge. mirror="x" gives the image across x = 0.
door = s3d.at(s3d.box("door", 30, 2, 60), x=15) s3d.transform(s3d.copy(door), rz=75) # opened about the hinge at x = 0 arm = s3d.at(s3d.extrude("arm", s3d.poly([(10, 0), (40, 0), (40, 20), (30, 20), (10, 6)]), 6), x=50) s3d.transform(s3d.copy(arm), mirror="x") # its image across x = 0
4 lines · 134 tokens (o200k_base)
A joint says how a part moves on another: on a frame read on the parent (ob.face, bbox, an assembly's anchor), about or along an axis: a direction of the parent's space, or "normal". Everything is built at rest; pose moves the mechanism, and export writes it as a URDF or a GLB nested by its joints. Each scene below is shown posed.
bbox(ob, which)Frame on its bounding box: a side "+X", an edge "+X-Y", a corner "+X-Y+Z"bbox(ob, "+X") is the centre of that side of the 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: the frame to hinge on. at(anchor=) takes the same directions.
cab = s3d.box("cab", 60, 60, 85) s3d.at(s3d.cylinder("side", 8, 12), on=s3d.bbox(cab, "+X")) # the centre of a side s3d.at(s3d.cylinder("edge", 8, 12), on=s3d.bbox(cab, "+X-Y")) # the middle of an edge s3d.at(s3d.cylinder("corner", 8, 12), on=s3d.bbox(cab, "+X-Y+Z")) # a corner s3d.at(s3d.box("cap", 20, 20, 10), on=s3d.bbox(cab, "+Z"), anchor="-Z+Y")
5 lines · 171 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 childpose(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 twoA lid revolute on the -Y edge of the top (degrees; the range contains 0), a wheel continuous, a drawer prismatic (the program's unit), a ball free. joint neither moves nor reparents the part; pose moves the mechanism. About +X, +Y turns toward +Z: a positive value lifts the lid.
chest = s3d.box("chest", 500, 400, 300) lid = s3d.at(s3d.box("lid", 500, 400, 20), z=300) s3d.joint("lid_hinge", lid, s3d.bbox(chest, "+Z-Y"), axis="+X", range=(0, 110)) wheel = s3d.at(s3d.cylinder("wheel", 160, 30), on=chest.face("+X").move(0, 150)) s3d.joint("wheel_spin", wheel, chest.face("+X").move(0, 150), kind="continuous") drawer = s3d.at(s3d.box("drawer", 180, 60, 300), on=s3d.bbox(chest, "-Y").move(y=-80).offset(-299)) s3d.seat(chest, drawer) # its bay, cut where it rests s3d.joint("drawer_slide", drawer, s3d.bbox(chest, "-Y"), kind="prismatic", axis="-Y", range=(0, 250)) ball = s3d.at(s3d.sphere("ball", 60), x=-320) s3d.joint("ball", ball, chest.face("top"), kind="free") # no axis, no range, no pose s3d.pose(lid, 70) s3d.pose(drawer, 180)
12 lines · 315 tokens (o200k_base)
Each joint is read on the part before: the elbow hangs on the lower arm and rides on the shoulder. pose(root, value, joint="name") poses a joint from any group above it.
base = s3d.cylinder("base", 160, 20) knuckles = [s3d.at(s3d.sphere("knuckle", 24), z=z) for z in (15, 329, 593)] lower = s3d.group("lower", s3d.at(s3d.cylinder("arm", 18, 300), z=34), knuckles[1]) upper = s3d.group("upper", s3d.at(s3d.cylinder("arm", 16, 250), z=348), knuckles[2]) head = s3d.at(s3d.cone("head", 110, 70, d2=50), z=612) s3d.joint("shoulder", lower, s3d.bbox(knuckles[0], "+Y").offset(-12), axis="+Y", range=(-30, 60)) s3d.joint("elbow", upper, s3d.bbox(knuckles[1], "+Y").offset(-12), axis="+Y", range=(-90, 90)) s3d.joint("wrist", head, s3d.bbox(knuckles[2], "+Y").offset(-12), axis="+Y", range=(-45, 45)) lamp = s3d.assembly("lamp", s3d.group("stand", base, knuckles[0]), lower, upper, head) s3d.pose(lamp, 35, joint="shoulder") s3d.pose(lamp, -80, joint="elbow")
11 lines · 329 tokens (o200k_base)
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) degreeslocal(ob, x, y, z, rx, ry, rz) is the frame at a point of the part's own space, turned as at turns: a source's joint origin, xyz and rpy, as written. One frame places the child (at(on=)) and hangs it (joint); the elbow is read in the arm's own space, wherever the arm stands.
base = s3d.cylinder("base", 160, 20) shoulder = s3d.local(base, z=20, ry=-30) # the arm rises 30° at rest arm = s3d.at(s3d.group("arm", s3d.at(s3d.box("beam", 300, 30, 30), x=150)), on=shoulder) s3d.joint("shoulder", arm, shoulder, axis="+Y", range=(-45, 90)) elbow = s3d.local(arm, x=300, rx=90) # 300 along the arm, its Z the arm's -Y hand = s3d.at(s3d.group("hand", s3d.at(s3d.box("rod", 200, 20, 20), x=100)), on=elbow) s3d.joint("elbow", hand, elbow, axis="normal", range=(-90, 90)) s3d.pose(arm, -20) s3d.pose(hand, -60)
9 lines · 229 tokens (o200k_base)
The leaf is a group: board and handle weld. copy carries the joint, the copy hangs on the same parent, and its mirror image moves as the image of the motion: both leaves open toward +Y.
post = s3d.at(s3d.box("post", 100, 100, 1500), x=-800) s3d.transform(s3d.copy(post), mirror="x") board = s3d.at(s3d.box("board", 750, 40, 1200), x=-375, z=150) handle = s3d.at(s3d.cylinder("handle", 20, 60), on=s3d.bbox(board, "+Y").move(x=315)) leaf = s3d.group("leaf", board, handle) s3d.joint("hinge", leaf, s3d.bbox(post, "+X").move(x=20), axis="+Z", range=(0, 90)) # on its +Y edge twin = s3d.transform(s3d.copy(leaf), mirror="x") s3d.pose(leaf, 60) s3d.pose(twin, 60)
9 lines · 204 tokens (o200k_base)
A joint that follows another takes ratio × its value and no range: a screw cap is a turn and a slide that follows it, 3 mm per turn; a small gear follows a big one at −2.
bottle = s3d.revolve("bottle", [(0, 0), (35, 0), (35, 140), (14, 175), (14, 200), (0, 200)]) cap = s3d.at(s3d.knurl("cap", 36, 24), z=190) s3d.joint("cap_turn", cap, bottle.face("top"), range=(0, 720)) s3d.joint("cap_lift", cap, bottle.face("top"), kind="prismatic", follows="cap_turn", ratio=3 / 360) s3d.pose(cap, 720, joint="cap_turn") # two turns: 6 mm up plate = s3d.at(s3d.box("plate", 260, 120, 10), x=250) big = s3d.at(s3d.gear("big", 30, 3, 12, bore=8), x=200, z=10) small = s3d.at(s3d.gear("small", 15, 3, 12, bore=8), x=267.5, z=10, rz=12) s3d.joint("drive", big, plate.face("top").move(200), kind="continuous") # the face starts at x = 0 s3d.joint("driven", small, plate.face("top").move(267.5), kind="continuous", follows=big, ratio=-2) s3d.pose(big, 20)
11 lines · 332 tokens (o200k_base)
seat(host, part)Take out of host the volume part fills, as it sits at rest; part untouchedseat(host, part) takes out of the host the volume the part fills at rest and leaves the part as it is: it may move. It cuts the exact shape: a knob turns in a round shank's seat, not a knurled one's.
console = s3d.box("console", 200, 120, 60) top = console.face("top") knob = s3d.group("knob", s3d.at(s3d.cylinder("shank", 24, 22), on=top.move(-50).offset(-12)), s3d.at(s3d.knurl("grip", 40, 24), on=top.move(-50).offset(10))) pin = s3d.at(s3d.cylinder("pin", 16, 60), on=top.move(50).offset(-40)) s3d.seat(console, knob) s3d.seat(console, pin) s3d.joint("pin_pull", pin, top.move(50), kind="prismatic", axis="+Z", range=(0, 60)) s3d.pose(pin, 45) # pulled out of its bore
9 lines · 194 tokens (o200k_base)
Ready-made parts, each standing on z = 0.
gear(name, teeth, module, width, bore=0, angle=20)Involute spur gearknurl(name, d, h, pitch=None, straight=False)Knurled grip; pitch 1 mm by defaultthread(name, d, length, pitch, left=False)Threaded rodgear(teeth, module, width) is an involute spur gear. knurl is a knurled grip, its pitch 1 mm unless given. thread is an ISO V-thread rod.
s3d.gear("gear", 20, 2, 10, bore=8) s3d.at(s3d.knurl("knurl", 20, 14), x=40) s3d.at(s3d.knurl("ribs", 20, 14, straight=True), x=70) s3d.at(s3d.thread("thread", 10, 24, 1.5), x=100)
4 lines · 96 tokens (o200k_base)
material says what a part is made of: one of 22 presets, its values changed where the call says. It tags the object and never touches its mesh; export writes it into the GLB.
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"Woods, metals, plastics, rubber, glass, ceramic, soft goods, stone, and light, a surface that gives light in its own colour. A wrong name raises and suggests the nearest one.
kinds = ["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"] for kind, place in zip(kinds, s3d.grid(6, 4, 80, -100)): s3d.material(s3d.at(s3d.sphere(kind, 40), on=place), kind) label = s3d.extrude("label", s3d.text(kind, 9), 1) s3d.material(s3d.at(label, on=place.move(y=-30)), "rubber")
7 lines · 189 tokens (o200k_base)
On a group or an assembly, material reaches every part under it that names none of its own: the nearest call wins, in whatever order the calls come. color= ("#rrggbb"), roughness=, metallic=, opacity= and emission= change the preset. The tag follows the part through cut, fillet and copy.
top = s3d.fillet(s3d.box("top", 1400, 800, 32), 3, edges="top") legs = s3d.at(s3d.box("leg", 60, 60, 718), on=s3d.grid(2, 2, 1260, 660)) table = s3d.assembly("table", s3d.at(top, z=718), legs) s3d.material(table, "walnut") # every part under it s3d.material(legs, "painted_metal", color="#2f5d50") # the nearest call wins vase = s3d.shell(s3d.revolve("vase", [(0, 0), (70, 0), (90, 120), (45, 260), (0, 260)]), 4, open="top") s3d.material(s3d.at(vase, x=-300, z=750), "glass", color="#3fae6a") bowl = s3d.shell(s3d.revolve("bowl", [(0, 0), (60, 0), (130, 80), (0, 80)]), 5, open="top") s3d.material(s3d.at(bowl, x=250, z=750), "ceramic") s3d.material(s3d.at(s3d.cylinder("cup", 70, 90), x=0, y=200, z=750), "brass")
10 lines · 331 tokens (o200k_base)
group(name, *children)Parent of childrenassembly(name, *children, **anchors)Parts and anchorsattach(handle, name, *children)A detail, under a handleHandle(root, **anchors)Root and anchorsexport(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 poseunits(name=None)Length unit: "mm" (default), "cm", "m"empty_scene()Fresh scenefingerprint()Digest of the sourceThey are shown at work in Assemblies. export(root, path) writes
in metres, whatever the program's unit: a GLB at the current pose, a URDF at rest with its meshes beside it, or
USD text (.usda) with the joints and the materials.
Write the plan first, then the details, then the parts. Each detail hangs on the plan's anchors, so deleting it deletes only it.
assembly(name, *parts, **anchors) returns a handle. The anchors are what the details need: a width, a height, a frame, a list of spots. Write them once, in the plan.attach. Delete its line and nothing else moves. A detail of a detail lives inside it: the buttons are added by add_back.add_back does with chair.seat_h.at(leg(), on=grid(2, 2, …)). A right side is transform(copy(left), mirror="x").Click a part on the left: it stands out in the model, and the functions that build it light up in the code.
1"""A chair, written as an assembly: the plan first, then the details, then the parts.2 3`chair` reads as a table of contents. `chair_frame` is the plan: the seat on its legs,4and the anchors the details need. Each `add_` function hangs one detail on those5anchors: delete its line and nothing else moves — without `add_back` it is a stool.6The parts come last, each built at its own origin, standing on z = 0.7"""8import sand3d as s3d9 10 11def chair(width=440, depth=420, seat_h=450):12 chair = chair_frame(width, depth, seat_h) # the plan13 add_back(chair) # the details, on the plan's anchors14 add_rails(chair)15 return chair.root16 17 18# ---------------------------------------------------------------- the plan19 20def chair_frame(width, depth, seat_h, t=30, inset=35):21 """The seat on four legs. Publishes the width, the seat's height and the line22 the back stands on."""23 legs = s3d.at(leg(seat_h - t), on=s3d.grid(2, 2, width - 2 * inset, depth - 2 * inset))24 seat = s3d.at(seat_board(width, depth, t), z=seat_h - t)25 return s3d.assembly("chair", seat, legs, width=width, depth=depth, seat_h=seat_h, inset=inset,26 back_y=depth / 2 - 20)27 28 29# ---------------------------------------------------------------- the details30 31def add_back(chair, h=420, t=24, button_d=16):32 """A panel standing on the seat's back edge, buttoned on its face."""33 back = s3d.at(back_panel(chair.width - 40, h, t), y=chair.back_y, z=chair.seat_h)34 # a side face's frame starts at z = 0, below the parent's origin, and its Y is up35 face = back.face("-Y").move(y=chair.seat_h + h * 0.6)36 buttons = s3d.at(s3d.dome("button", button_d, 5), on=s3d.grid(3, 2, 100, 110, on=face))37 s3d.attach(chair, "back", back, buttons)38 39 40def add_rails(chair, d=18, z=160):41 """A rail between the front and back legs, on each side."""42 # both=True: centred on its middle, so turned about X it spans the depth evenly43 rail = s3d.extrude("rail", s3d.circle(d), chair.depth - 2 * chair.inset, both=True)44 sides = s3d.grid(2, 1, chair.width - 2 * chair.inset, on=s3d.Frame((0, 0, z)))45 s3d.attach(chair, "rails", s3d.at(rail, rx=90, on=sides))46 47 48# ---------------------------------------------------------------- the parts49 50def seat_board(width, depth, t):51 return s3d.fillet(s3d.box("seat", width, depth, t), 8, edges="top")52 53 54def leg(h, foot=26, top=36):55 """Tapered, its foot on z = 0."""56 return s3d.cone("leg", foot, h, d2=top)57 58 59def back_panel(width, h, t):60 return s3d.fillet(s3d.box("back", width, t, h), 6, edges="top")
60 lines · 776 tokens (o200k_base)
The plan publishes head, the frame at the top of the stem. The shade sits on it with on=lamp.head: a longer stem moves the shade with it.
1import sand3d as s3d2 3 4def lamp(stem_h=300):5 base = s3d.revolve("base", [(0, 0), (70, 0), (60, 20), (0, 20)])6 stem = s3d.at(s3d.cylinder("stem", 12, stem_h), on=base.face("top"))7 lamp = s3d.assembly("lamp", base, stem, head=s3d.Frame((0, 0, 20 + stem_h)))8 add_shade(lamp)9 return lamp10 11 12def add_shade(lamp, d=180):13 shade = s3d.shell(s3d.cone("shade", d, 120, d2=d / 2), 1.5, open="top bottom")14 s3d.attach(lamp, "shade", s3d.at(shade, on=lamp.head))
14 lines · 194 tokens (o200k_base)
A buttoned back rolling over at the top, a domed seat on a nailed apron, four tapered legs. The architecture is the seat on its legs; the back and every row of trim are details; the parts come last, in the order they are called. The legs are lofts through their sections, the right ones images of the left.
1"""A tufted chair: a buttoned back rolling over at the top, a domed seat on a nailed2apron, four tapered legs.3 4Millimetres, Z up, the chair's front towards -Y; built as its left half's legs, the5right ones mirrored. The architecture is the seat on its legs; the back and every6row of trim are details added to it: deleting `add_back` leaves a stool, and the7back's tufts follow the width they are given.8"""9import json10import math11import sys12 13import sand3d as s3d14 15 16def tufted_chair(width=520):17 chair = chair_frame(width)18 add_back(chair)19 add_seat_nails(chair)20 return chair.root21 22 23def chair_frame(width=520, depth=480, seat_z=430, inset=50, floor=0):24 seat = upholstered_seat(width, depth)25 legs = []26 for leg, y in ((front_leg, -(depth / 2 - inset)), (rear_leg, depth / 2 - inset)):27 left = s3d.at(leg(), x=-(width / 2 - inset), y=y, z=floor)28 legs += [left, s3d.transform(s3d.copy(left), mirror="x")] # the right one: its image across x = 029 return s3d.assembly("chair", s3d.at(seat.root, z=seat_z), *legs,30 seat=seat, width=width, seat_z=seat_z, rear_y=depth / 2)31 32 33def add_back(chair, behind=-30, above=200):34 back = tufted_back(chair.width)35 s3d.attach(chair, "back", s3d.at(back.root, y=chair.rear_y + behind, z=chair.seat_z + above))36 add_buttons(back)37 add_back_nails(back)38 39 40def add_seat_nails(chair, pitch=22, d=10, h=5):41 """One nail every `pitch` along the seat's seam, round its front and sides."""42 seat = chair.seat43 spots = s3d.snap(seat.parts, s3d.along(seat.seam, pitch))44 s3d.attach(seat, "seat_nails", *[s3d.at(s3d.dome("nail", d, h), on=f) for f in spots])45 46 47def add_buttons(back, d=16, h=3):48 s3d.attach(back, "buttons", *[s3d.at(s3d.dome("button", d, h), on=f) for f in back.tufts])49 50 51def add_back_nails(back, pitch=22, d=10, h=5, face=((220, -230), (226, 150)), reach=400):52 """A row up each side of the back's face, where its cover is nailed to the frame,53 from (x, z) to (x, z), found from in front, `reach` away."""54 spots = []55 for side in (-1, 1):56 (x0, z0), (x1, z1) = face57 spots += s3d.snap(back.parts, s3d.along([(side * x0, -reach, z0), (side * x1, -reach, z1)], pitch),58 direction=(0, 1, 0))59 s3d.attach(back, "back_nails", *[s3d.at(s3d.dome("nail", d, h), on=f) for f in spots])60 61 62# ---------------------------------------------------------------- parts, in the order they are called63 64def section(z, w, d, x=0, y=0, n=2, taper=0):65 """One cross-section of a loft: a superellipse `w` wide and `d` deep, squareness66 `n` (2 an ellipse, 4 a box with round corners), its +Y side narrowed by `taper`,67 centred at (x, y, z)."""68 return s3d.at(s3d.superellipse(w, d, n, taper), x=x, y=y, z=z)69 70 71def upholstered_seat(width=520, depth=480, taper=0.04, squareness=6, board=-8,72 crown=((15, 3, 3, 0), (35, 12, 10, 4), (55, 30, 24, 8), (68, 90, 72, 12), (76, 190, 160, 18)),73 apron=60, dip=15, wall=8, seam_up=6, seam_out=10):74 """A cushion domed over a board, an apron hanging round it. `crown` is the dome,75 level by level: (height, inset at the sides, inset front and back, shift back)."""76 w = width / (1 + taper) # the loft's mean width; the front edge is `width`77 cushion = s3d.loft("cushion", section(board, w, depth, n=squareness, taper=taper),78 *[section(z, w - 2 * side, depth - 2 * end, y=back, n=squareness, taper=taper)79 for z, side, end, back in crown])80 81 def drop(x): # how far the apron hangs below the board: deepest mid-front82 return apron + dip * math.cos(math.pi * x / width) ** 283 84 hem = s3d.superellipse(w - wall, depth - wall, squareness, taper).loops[0].pts85 skirt = s3d.loft("apron", *[s3d.at(s3d.superellipse(drop(x), wall, squareness), x=x, y=y, z=board - drop(x) / 2,86 ry=-90) for x, y in hem], follow=True, loop=True)87 # the seam the nails follow: just outside the apron, round the front and sides88 # but not the back, which the back of the chair covers — from the middle of the back round89 rim = s3d.superellipse(w + 2 * seam_out, depth + 2 * seam_out, squareness, taper).loops[0].pts90 start = min(range(len(rim)), key=lambda i: abs(math.atan2(rim[i][1], rim[i][0]) - math.pi / 2))91 seam = [(x, y, board - drop(x) + seam_up) for x, y in rim[start:] + rim[:start] if y < depth / 2 - seam_out]92 return s3d.assembly("seat", cushion, skirt, parts=[cushion, skirt], seam=seam)93 94 95def front_leg(height=420, profile=((0, -10, 30, 30), (210, -4, 36, 36), (420, 0, 44, 44))):96 """A square leg with round corners, tapering to its foot, which steps forward.97 `profile` is (z, y, width, depth) from the floor up; its origin is under its foot."""98 body = s3d.loft("front_leg", *[section(z * height / profile[-1][0], w, d, y=y, n=4) for z, y, w, d in profile])99 return s3d.group("front_leg", s3d.at(body, y=-profile[0][1]))100 101 102def rear_leg(height=420, profile=((0, 16, 30, 30), (210, 6, 36, 36), (420, 0, 44, 44))):103 """The back leg, raking back to its foot. Its origin is under its foot."""104 body = s3d.loft("rear_leg", *[section(z * height / profile[-1][0], w, d, y=y, n=4) for z, y, w, d in profile])105 return s3d.group("rear_leg", s3d.at(body, y=-profile[0][1]))106 107 108def tufted_back(width=520, drawn_for=520, squareness=4, profile=(109 (-260, 0, 440, 50), # foot, behind the seat: z, y, width, depth110 (-140, 6, 460, 60),111 (-20, 14, 470, 64), # the cushioned middle112 (90, 24, 480, 66),113 (170, 36, 490, 72), # leaning back114 (205, 58, 490, 70), # over the top115 (180, 82, 490, 60), # down the back116 (130, 88, 490, 46)), # tucked under117 tuft_rows=((-60, -120), (-52, -40), (-44, 40), (-34, 120)), tuft_pitch=80, tuft_margin=100, tuft_r=55,118 tuft_depth=20):119 """A cushioned panel rising from the seat and rolling over backwards, one loft120 along its own centre line; buttoned on a diamond lattice, each tuft row (y, z),121 the rows' columns staggered. The profile's widths are those of a `drawn_for` chair."""122 widen = width - drawn_for123 body = s3d.loft("back", *[section(z, w + widen, d, y=y, n=squareness) for z, y, w, d in profile], follow=True)124 reach = int((width / 2 - tuft_margin) / tuft_pitch) # columns each side of the middle125 lattice = [(j * tuft_pitch, y, z) for i, (y, z) in enumerate(tuft_rows)126 for j in range(-reach, reach + 1) if (i + j) % 2 == 0]127 tufts = s3d.snap(body, lattice)128 s3d.dent(body, tufts, tuft_r, tuft_depth)129 return s3d.assembly("tufted_back", body, parts=[body], tufts=s3d.snap(body, tufts))130 131 132if __name__ == "__main__":133 s3d.empty_scene()134 s3d.export(s3d.group("tufted_chair", tufted_chair(**json.loads(sys.argv[2] if len(sys.argv) > 2 else "{}"))),135 sys.argv[1])
135 lines · 2304 tokens (o200k_base)
wire closes its path with closed=, loft its ring
with loop=.docs/api.md, 3,643 tokens (o200k_base). This page is for people.tools/quickstart from the code it shows