Skip to content

Structure and code-CAD

Stage 2 · Mechanics · ~35 min · Before this: 05 Leg loads and stairs, 07 Mass budget and proportions · numbers from the repository as of 2026-11-02

A humanoid’s frame has to carry landing loads several times its weight, fit between motors, be made on a desktop mill or printer, and weigh as little as possible. Marvin Bot draws every part in Python code, computes each part’s mass straight into the robot model, and checks every load-carrying part with a hand calculation. This page explains how, and lets you size a plate and sweep a joint yourself.

The structure is the robot’s skeleton: the metal and plastic parts that hold the motors in place and carry the robot’s weight from the feet up to the pelvis. A humanoid needs it to be strong enough not to bend or snap when it lands or falls, yet as light as possible, because every gram of frame is a gram the motors must carry (07 Mass budget). Code-CAD is how Marvin Bot draws those parts: instead of clicking shapes together in a drawing program, each part is written as a short Python program.

When Marvin Bot lands from a step, the ground pushes up on the foot with up to about three times the robot’s weight. That force has to travel through the foot plate, the ankle, the shin, the knee, the thigh, the hip and into the pelvis: that chain of parts is the load path. Wherever the force’s line does not pass straight through a part, it bends it, and bending is what breaks thin plates.

Try it with a plastic ruler: lying flat, it bends easily under a finger; stood on its edge, you can hardly bend it at all. A plate bent across its thickness is weak; the same plate bent across its width is strong. The trick of structural design is to arrange material so that every load bends parts the strong way, and to remove material where the stress is low. The first playground lets you see how much difference thickness, pockets and material make.

The words in the readouts, in plain terms (the maths follows in From first principles):

  • Bending moment M: how hard the load tries to bend the plate at the checked point; force × lever arm, in N·m.
  • Second moment I: how well the plate’s cross-section resists bending, in mm⁴. It depends on shape only, not material; thicker is much better.
  • Peak stress σ: the force per unit area inside the metal at its worst point, in MPa (newtons per square millimetre).
  • Safety factor: the material’s yield strength (the stress at which it starts to bend permanently) divided by the peak stress. Below 1 the part is permanently bent; Marvin Bot wants at least 2 (green). Amber or red means too weak.
  • Deflection: how far the loaded end moves, in mm. Smaller is stiffer.

A plate is clamped at one end (the station where we check it) and a load acts at a lever arm L from that station, like the thigh plate in a sideways fall onto the knee. Choose the cross-section, material and load; the readouts give the bending stress, safety factor, tip deflection and mass.

Presets

Section

How wide the plate is, side to side.
Stock thickness; the desktop mill takes up to 60 mm.
Thickness left at the bottom of each milled-out pocket.
The rim and ribs left at full thickness between pockets.

Material

Load

Repo cases: landing 3 × up (with 0.5 fore-aft, 0.3 side), side fall 1 ×, hand catch 0.5 ×.
Distance from the checked station to the load's line.
1 = plain hand calculation (what cad/strength.py does). Holes and sharp corners concentrate stress locally, like a tear starting at a nick in paper: 2-3 × higher.
Bending moment M
–
Second moment I
–
Peak stress σ
–
Safety factor (target 2)
–
Deflection at load
–
Mass per 100 mm
–

Most mechanical design is done in graphical CAD (computer-aided design) programs: you sketch and extrude with a mouse, and the model lives in a binary file. Code-CAD writes the same solids as a program: a plate is box(...), a motor seat is cyl(...), and a part is the union or difference of such shapes. Marvin Bot draws its machined parts in build123d, Python on the OpenCascade kernel, with exact outlines and curved surfaces and export to STEP, the standard file format machine shops accept.[2] The shells, covers and quick checks use manifold3d, a fast, robust library for boolean operations (join, subtract, intersect) on triangle meshes (surfaces made of many small triangles).[1] Alternatives in the same spirit: CadQuery (also on OpenCascade)[3] and OpenSCAD (its own language, mesh-based).[4]

Why code for a robot in its simulation phase:

  • Parametric by construction. The parts read their sizes from description/robot.yaml and skeleton.yaml. When the legs grew 50 mm on 2026-10-29, the thigh and shin regenerated; nobody redrew them.
  • Diffable. A change is a few lines in a pull request (a proposed change to the code, reviewed before it is accepted) (thigh_t = 9.0), reviewable like any code, not an opaque binary.
  • One source of truth for mass. cad/build.py computes each part’s mass, centre of mass and inertia and writes them into description/cad_model.yaml; the URDF (the robot model file the simulators read) and the mass budget take them from there (13 The robot description). The simulator therefore trains on the masses of the parts that will be machined.
  • Checks run on every change. Size against the mill and printer, strength, packaging and shell collisions are all scripts.

The cost: fine detail (fillets, fasteners) is slower to write than to click, and styled surfaces are hard to get right in code: today’s shells are generated as an offset hull round each link’s parts, so the frame sets their shape. They are to be redrawn in build123d as designed, lofted panels (docs/decisions.md, 2026-10-31). Fasteners are a budget item until a fastener pass.

# cad/leg.py, the thigh plate (simplified excerpt, build123d)
tplate = rounded(hull(rect(-32, 32, -60, fl_bot), disc(124, xz(knee)))) # outline, rounded inside corners
k_clip = clip_spot(tplate, xz(knee), [stator_holes("RS04", knee, "y")], min_r=64) # harness clip
tplate = cut(tplate, stator_holes("RS04", knee, "y"), # the motor's bolt circle
lighten(tplate, keep=[ring_band("RS04", knee, "y"), disc(CLIP_PAD, k_clip)]), # pockets
disc(CLIP_HOLE, k_clip)) # the push-in clip's hole
out.append(made("left_thigh_plate", "left_thigh_link", "al7075", plate(tplate, "y", y, tt)))
Material Yield / strength (MPa) Density (g/cm³) Stiffness E (GPa) Used for in Marvin Bot
6061-T6 aluminium 276 typical[5] (repo design value 240) 2.70 68.9 motor rings and hubs, spacers (37 of 237 CAD parts)
7075-T6 aluminium 503 typical[6] (repo 480) 2.81 71.7 most of the frame: plates, brackets, flanges, cranks, straps (125 parts)
PA6-CF, printed (carbon-fibre nylon) 82 in layer, 64 across layers[7] (repo 25 across layers) 1.17 (repo 1.10) 5.7 / 4.7 battery latch and guides, jigs (the body shells and head panels are white PETG)
PETG, printed ≈ 47-50[8] 1.27 ≈ 2 not used for structure: softens near warm motors
TPU, printed flexible ≈ 1.15 — 3 mm foot soles, harness edge clips

How to read the table: yield is the stress at which the material starts to bend permanently (higher is stronger), density is mass per volume (lower is lighter), and stiffness E is how much it resists stretching (higher bends less). “T6” names a heat treatment that hardens the aluminium. PA6-CF is nylon filled with chopped carbon fibre; PETG and TPU are common 3D-printing plastics (TPU is rubbery).

Aluminium is about 2.3 times as dense as PA-CF but 3 to 10 times as strong and 12 to 15 times as stiff, so for parts that carry the robot’s weight it wins per gram. Printed parts win on shape (hollow shells, curves, internal cable runs) and cost, but nothing printed carries load any more: since 2026-10-30 the arms carry their load in 7075 straps and every load-carrying part is aluminium, and printing is kept for the shells and head panels (white PETG) and small fittings.

Printed parts are weakest between layers, much as plywood splits along its layers: a part printed standing up and bent so the layers peel apart may hold only 25-40 MPa, which is the value cad/fea.py uses.

The workshop sets the shapes. Every machined plate is flat (2.5D: an outline and pockets from one face), so the mill cuts it with 2D toolpaths. Machined parts must fit the desktop mill with its tool changer fitted: 190 × 210 or 240 × 160 mm, depending on which side the changer sits, and 60 mm stock (MILL_MM in cad/build.py). Printed parts must fit a 250 mm printer. Flat parts too large for the mill (the 230 mm foot plates, the 265 mm torso side frames) go to an online laser or water-jet service. This is why the thigh grew 25 mm above the hip yaw motor rather than below it on 2026-10-29: the thigh plate still fits the mill.

Each joint on a quasi-direct-drive robot (see 04 Actuators) hangs the next link on the motor’s output. If the link is held on one side only, a sideways load bends the plate out of its plane and puts a moment on the motor’s output bearing. Think of a shelf held by one bracket versus a shelf held at both ends.

Marvin Bot’s first-pass leg did exactly that, and the strength check failed five parts at landing load: the foot base plate (SF 0.3), hip yoke back plate (0.3), hip roll front plate (0.6), shin knee flange (0.8) and hip yoke side plate (1.4) (docs/decisions.md, 2026-10-26).

The second pass added a far-side bearing at the knee and hip roll: the link becomes a yoke held on both sides, the load splits between two plates and the bending in each falls several-fold. The worst safety factor went to 2.4. Later, with finite elements (below), thin crossed-roller bearings went into rings at the hip yaw, the waist and the front of each hip roll motor, so those joints’ tilting loads reach the frame without passing through the motors’ own output bearings (2026-10-30).

Cut a plate at a station. A force F acting a distance L away makes a bending moment M = F · L (N·mm if L is in mm). The moment stretches one face and squeezes the other; the stress grows linearly from zero at the neutral axis (through the section’s centroid) to its peak at the face furthest away, a distance c (the centroid is the section’s balance point; for a plain rectangle, its middle):[9]

σ = M · c / I

σ is stress in MPa (N/mm²). I is the section’s second moment of area (mm⁴): each bit of area times the square of its distance from the neutral axis, summed. For a solid rectangle of width b bent across its thickness t,

I = b · t³ / 12, c = t / 2

so stress falls with thickness squared: double the thickness and the stress drops to a quarter. Bend the same plate across its width and t and b swap places: a 59 × 9 mm plate is about 43 times stiffer and 6.6 times stronger in plane than out of plane.

Pocketing. Material near the neutral axis does little, because it is multiplied by a small distance squared. Milling pockets from one face down to a thin floor removes that material but leaves full-depth ribs and rims, a section like a channel or an I-beam. Drilling holes right through removes the outer fibres too, which are the ones doing the work. The playground shows the difference: at the same mass, a pocketed plate has a much larger I than a thinner drilled one.

Safety factor. SF = yield strength / peak stress. SF 1 means the part just starts to bend permanently. Marvin Bot targets SF 2 on yield because the hand calculation leaves out torsion and stress concentrations at holes and corners, which typically raise local stress by 2-3 times[10], and because the load cases are estimates until falls are measured.

A joint’s range is set by the first angle at which something hits. This matters because the robot’s controller must never command an angle that drives an arm into its own chest: the limits found here go into the robot model that the simulator and the trained controller use. This toy has the upper arm (a box) with the shoulder yaw motor (a cylinder) near its top, hanging from the shoulder roll axis beside a rounded torso and a head. Pick the shoulder pitch (arm forward) and sweep the shoulder roll inwards: the toy finds the first contact, and the chart repeats it for every pitch angle. It is a simplified version of what python -m cad.covers --check and tools/check_packaging.py do with the real meshes.

Presets

Pose

+ = arm away from the body; − = towards the midline.

Geometry

A rounder chest leaves room for the arm to come forward and in.
The shoulder yaw RS10P is Ø57 mm; its ring and the front plate's legs reach Ø92 mm.
The body shells sit 4 mm off the parts (cad/covers.py).
This pose
–
First contact, sweeping roll inward
–
Limit, arm hanging (repo 0°)
–
Limit, arm forward 90° (repo −20°)
–

How to read this picture: the top panel shows the arm and torso from the front and from above, with anything touching marked red. The chart below has shoulder pitch (how far the arm is raised forward) along the bottom and the lowest shoulder roll reached before contact up the side; below the curve the arm would hit the body.

toy: lowest roll before contactrepo coupled limit (robot.yaml)contact
Choice Taken Alternatives considered (repo)
Structure all-aluminium flat plates on the load path (mostly 7075, pocketed), printed shells as an add-on printed structural shells on the arms and chest (retired 2026-10-30); fully printed (load must still go through metal; heat near the motors softens PETG-class plastics) (2026-10-25)
Joint support far-side bearings at the knee; crossed-roller bearings at the hip yaw, waist and hip roll; hip pitch as a closed box single-sided joints, which need the RS04 output bearing’s moment rating (not taken, 2026-10-28); the hip roll’s rear bearing (about 300 g, replaced by the front carrier)
Thigh plate 9 mm pocketed to 2 mm floor (+61 g a side) 8 mm (SF 1.8); a topology-optimised outline could take back about 35 g
CAD build123d for machined parts (exact outlines, STEP files), manifold3d for shells and covers today; shells to move to build123d lofted panels CadQuery, OpenSCAD, graphical CAD; Blender or implicit modelling for the shells
Big flat parts online laser or water-jet service; about 12 parts to online machining (~$1,200, not yet quoted) a larger mill; splitting parts into pinned segments
Strength check finite elements for every load-carrying part (cad/fea.py), SF 2.2 hand calculation on CAD sections (missed torsion; retired 2026-10-30)
  • Plate thickness, pockets and material, given the worst load case and the SF 2.2 target. Trade: each millimetre of plate adds strength and mass; python -m cad.fea --link <link> --where shows where a part’s peak is.
  • Machined, printed or sent to a service. Trade: printed is cheaper, lighter and freer in shape but far weaker (especially between layers); machined is strong but limited to the mill’s area; a service handles bigger flat parts at extra cost and lead time.
  • Where a joint needs a far-side bearing. Trade: an extra bearing, plate and a little mass, in exchange for several-fold less bending in each plate and no sideways load on the motor’s own bearing.
  • Joint range versus body shape. Trade: a rounder chest or a slimmer motor gives the arm more room; otherwise accept a coupled limit that the controller must respect.
Your decision note

Built from your settings above. Paste it into an issue, a design discussion or an AI assistant session.

Next: 09 Power and battery starts the electrical stage, beginning with the battery, the single heaviest item in the mass budget that is not a motor or frame.

  1. E. Lalish et al., Manifold: geometry library for topological robustness — used for: what manifold3d is (mesh booleans, Python bindings)
  2. build123d documentation — used for: the machined parts’ code-CAD (Python B-rep on OpenCascade)
  3. CadQuery documentation — used for: Python CAD alternative with STEP export
  4. OpenSCAD — used for: script-based CAD alternative
  5. Aerospace Metals, Aluminum 6061-T6 / T651 datasheet (ASM / Aluminum Association data) — used for: 6061-T6 yield 276 MPa, density 2.70, E 68.9 GPa
  6. Quickparts, Aluminum 7075 datasheet (ASM data) — used for: 7075-T6 yield 503 MPa, density 2.81, E 71.7 GPa
  7. Polymaker, PolyMide PA6-CF technical data sheet V4.1 — used for: printed PA6-CF tensile strength (XY 82, Z 64 MPa conditioned), modulus, density 1.17
  8. Prusa Polymers, Prusament PETG technical data sheet — used for: PETG printed yield 47-50 MPa, density 1.27
  9. MIT OpenCourseWare, 2.001 Mechanics and Materials I — used for: beam bending σ = M c / I, second moment of area, cantilever deflection
  10. W. D. Pilkey, D. F. Pilkey, Z. Bi, Peterson’s Stress Concentration Factors, 4th ed., Wiley 2020 — used for: typical stress concentration factors of 2-3 at holes and notches
  11. M. F. Ashby, Materials Selection in Mechanical Design, 4th ed., 2011 — used for: strength and stiffness per mass comparison