Build plans · CoScad

Build plans: coscad plan

coscad plan foo.assemble turns an assembly into a numbered build sequence a person can follow without backtracking: nuts go in before the slot they need is consumed, every screw is driven with its driver clear, each step names which face rests on the bench, and every fastener carries a torque for its host's material. The design rationale is in the assembly instruction generator design note; this file is the working reference.

What you write

Placement can come from an asm expression or from fixed / mate constraints. The planner uses the same solved poses as the assembly loader and checker. Beyond placement, it needs a few part hints and one fastener line per screw group:

rx ← rail200.coscad ×0 profile=2020 material=aluminium
rz ← rail160.coscad ×0 profile=2020 material=printed ends=open,blocked
br ← flat90.coscad ×16

fastener M5x10 br#1 rx#1 bot at=30            // spec clamped host face [options]
fastener M5x10 px  ry#2 rt ×2                 // two screws, spaced along the overlap
fastener M5x10 br1 railA top through=6        // counterbored bracket: 6 mm under the head
plan beam=30 rest=bot,top,fwd,bak             // optional planner settings

Part names must be unique. Repeated options on a part or fastener are errors, including repeated ×count or ▽orientation. Unknown fastener options are errors; unknown part hints such as seam=rear remain available as metadata. Recognized hints are checked before planning: profile must be 2020, and ends must contain exactly two open or blocked values.

Distances in at=, through=, and inset= must be finite and non-negative. Screw diameter and length must be positive; write peg for a friction joint. Fastener quantities are from 1 to 10,000. Part print quantities are from 0 to 10,000, with a total expanded print quantity of at most 10,000 across recursive assemblies. Expanded fasteners also total at most 10,000, including explicit at= positions. These are input limits, not a promise that planning a large assembly will be fast.

plate width depth [margin] requires finite positive dimensions and a finite, non-negative margin that leaves usable space on both axes; the default margin is 6 mm. clearance mm requires a finite, non-negative distance.

Driver access is judged on boxes: a driver is blocked only when its cylinder really enters another part's oriented box (the part's own box under its placement, so a diagonal strut is a thin rod, not the block its axis-aligned box would be), and touching does not count, so a shell whose box brushes the neighbouring shell's box, as in the ball, does not block the screw on the other side. A part may be placed "held" against anything already placed that it will be joined to, on either side of the joint. A slender part (three times longer than wide) stood on end costs the same as a rail stood on end and gets the same warning.

The planner derives the driver axis (the host face's outward normal), the screw positions, the thread reach (length minus material under the head), and the T-nut preload sheet. Along the host, positions use the overlap or explicit at= distances. Across a profile=2020 side face, they stay on the host's slot centre even when an inset panel covers only part of the face. A clamped part that does not cover that centre is rejected. Other hosts and rail end faces use the transverse overlap centre.

The 200 mm cube is a worked constraint-based assembly: both holes of each of its 16 brackets are mated to rail interfaces, and its six panel bores match explicit screw distances. See the geometry and hardware verification.

Frame-based screws and bearing seats

Use a named host frame instead of a world face when the joint has an explicit bore. Each line describes one physical screw:

fastener M4x12 arm center @root_hole seat=@head_seat head=socket nut=hex pocket=@nut_seat

The host frame origin is the hole's interface point; its outward +Z axis points toward the screw head. seat=@head_seat selects a frame on the clamped part at the underside of the head, including any counterbore. Its normal points in the same direction. pocket=@nut_seat selects the nut's bearing seat on the host, with its normal pointing away from the head. pocket=clamped.@nut_seat selects a seat on the clamped part instead. These are bearing surfaces, not the centres of the screw head or nut.

The solved part poses carry every point and direction into world coordinates. The planner checks that the frames are coaxial, their normals agree, and seats lie within their parts' bounds. Screw reach is the screw length minus the distance between the head and nut bearing seats. For a T-nut or threaded host, it measures from the head seat to the host interface. Existing minimum-reach checks still apply. Frame-based rail holes must lie on the 2020 slot centre and outer face. at=, ×count, through= and inset= cannot be combined with a host frame; they would provide a second source of placement or seating information. Pegs retain their face/inset syntax.

preload=hand on a part prepares its nuts while it is independent, before placing that part. This is useful for opposing rail slots or underside pockets: turn the loose part to reach them and retain the nuts during placement. The default preload=placed keeps the existing access checks against the bench and previously placed parts. This hint declares a preparation operation, not a fixed assembly order.

The generated JSON includes axis, head_position_mm, thread_reach_mm, and, for a framed nut pocket, nut_seat_mm, alongside position_mm. Step renders and driver-clearance checks start at the same head seat. No model names, coordinates, screw counts or assembly sequences are built into the planner. Bore and seat frames and hardware specifications are explicit design metadata; the planner does not infer threads or discover holes from arbitrary boolean geometry. Bounds checks are not proof that a bore is empty or that a real tool fits: verify the part geometry too.

Worked examples: the repaired rail corner uses two closed mounting interfaces per outer bracket and six M5 screws; the elastic-cord toy bow uses four M4x12 screws, four captive nuts and recessed head seats. The bow's 8 mm bearing-seat distance leaves 4 mm of screw past the nut bearing face.

Joints and how parts go in

Before searching, coscad plan meshes every part with the native kernel and measures, once, everything the search asks about shape:

The search also refuses any move after which a part still to place has no way in left: placed parts only accumulate, so such a state is a dead end whatever the rest face.

Rest faces are the six world faces plus, when the assembly is not square to the world, the six faces of its largest part ("the block's bottom face down").

What it checks

Design errors (reported, and the run exits 1):

Hard predicates on every step: a free part must rest on the bench or on placed parts and balance on that support (a part about to be joined may be held against its host); nothing may reach under the partial assembly (flip instead); the assembly's centre of mass stays over its bench contact; the part has a clear way in that keeps to the axis of any screw joint it makes, does not come up through the bench (far enough to clear what it touches) and leaves a fingertip room to push it home; a screw joint's moving part turns clear of everything placed and can be gripped; a screw's driver cylinder is clear of everything but its own parts and does not come up through the bench; T-nuts can still enter (drop-in: the slot face is exposed; slide-in: an end is open and unsealed).

Holding: after each move, a placed part stays put on its own if it is on the bench, or (repeatedly) joined by a made joint that holds, resting balanced on, or captured by (every way out blocked) parts already known to stay. Every other piece (parts joined together count once) needs a hand. One hand does each move. While the assembly turns, gravity holds nothing: pieces not joined to or captured by the heaviest piece must be held, and the two hands turning it can each press one.

Costs: each move records its terms, and the plan cost is each term times its weight (plan options; the defaults are guesses until calibrated with coscad calibrate, after which they are seconds):

term counted default
flip per flip 10
vertical per move with a host rail, or a rail or slender part still loose, standing 6
driver per screw driven more than driver_angle off vertical 3
shrink per move that shrinks the bench footprint 2
sibling per screw that leaves another on the same part undone 1
loose per loose piece after each move 1
insert per bit of joint alignment work 0.5
turn per turn of a screw joint 0.3
hand per hand needed beyond two, per move or turn 20
finger per move, times the fraction of the finger room other parts take 2
part, fastener, nut base handling per part, joint and nut batch 0

insert, turn, part, fastener and nut are the joints' own work, about equal for every order: the phase search's "cheap move" test ignores them, and the completion estimate includes what is left of them.

Outputs

Companion site

coscad site DIR a.assemble b.assemble ... plans each assembly with renders and writes a static site: DIR/index.html (search across builds by name, part, material, screw spec) and DIR/<name>/index.html (step cards with the render, rest face, parts, preloads, screws with torque, warnings; prev/next buttons, arrow keys, a filter box, #step-N links). It is plain HTML with the plan JSON embedded, so it works from a file or any static host (GitHub Pages, a folder on the router box). Light and dark themes, safe-area padding for phones.

How the search works

Nodes are part instances, one T-nut batch per rail, and fasteners; edges are requires-before relations derived from the model (host and clamped part before a screw, a rail's nuts before its screws and before the parts clamped to it, slide-in nuts before anything sealing an end). Another part covering a drop-in slot or a hex pocket is not an edge: nutCanEnter rejects the nuts once the entry is covered, and the order works because settling loads a host's nuts as soon as it is placed (see "Feature burial" under Planned). A state is the placed set plus the rest face. Placing a part settles its follow-ups (nuts that can now go in, screws whose driver is clear). The search is a beam over rest-face phases: on one face it greedily takes every cheap move; it branches on flipping to another face or on accepting one expensive move. A completion heuristic charges for parts that will need a flip to reach and for screw groups that cannot be vertical on the current face.

Search width matters on larger frames. For the 29-part cube fixture the plan cost was 208 / 183 / 179 / 167 at beam 10 / 30 / 80 / 200, in 3 / 10 / 25 / 60 seconds; the corner example is instant at any width.

Limits

Planned

Not implemented yet. These four extensions come from a separate prototype planner (AsmPlan.hs) that modelled joints, buried features, staging areas and a doorway on a desk build. The prototype's geometry was a toy; the ideas below are the parts worth carrying over. Proposed syntax follows the existing .assemble conventions and may change.

Feature burial as one rule

Done for part paths, finger room and screw joints turning on 2026-10-29 (see "Joints and how parts go in" and PLAN-EASE). Nut slots, pockets and driver cylinders still use the special cases below.

Today the edges are special cases: clamped part after its nuts, slide-in nuts before end caps, and a predicate (not an edge) for drop-in slots and hex pockets. Replace them with one rule:

Node A must come before part P when A needs access to a region that P occupies once placed.

The regions the model already computes are the access regions:

For every access region and every part whose oriented box enters it (other than the node's own host and clamped part), add the edge. The edges do not depend on the rest face; the bench covering a face stays a predicate, because a flip clears it. A cycle among these edges is a design error that names the feature, for example "the top slot of bottom_front is covered by panel, but panel's screw needs a nut in that slot". This turns today's dead ends in the search into errors reported before planning, and the extra edges also prune the search.

Operations

Steps that join nothing: square a frame, check a diagonal, test a mechanism before it is enclosed. One line per operation:

op square "Check both diagonals; tap the corners square" over=bottom_front,bottom_back,bottom_left,bottom_right before=panel
op check "Spin the rotor by hand; it must turn freely" over=rotor#1 uses=rotor#1.top

Fasteners that join over= parts are driven in two passes: snug when placed ("finger tight, then a quarter turn"), and to their torque in a "Torque" step after the operation. Assemblies with no op lines plan exactly as now. Operations are nodes in the requires-before graph with no geometry of their own; they never block a driver or a nut.

Sub-assemblies

The planner grows one assembly on one bench. Many builds are faster and steadier as separate pieces joined at the end: two side frames, then the rails between them.

sub left  = upright_fl, upright_bl, bottom_left, top_left, br_front_low_left, br_back_low_left
sub right = upright_fr, upright_br, bottom_right, top_right, br_front_low_right, br_back_low_right

Discovering sub-assemblies (connected groups that need nothing from the rest) can come later, once declared ones work.

Workspace and delivery

Two plan options:

plan bench=900x600 floor=2000x2000
plan door=690x2040