Standard parts and CAD geometry¶
Every cell has scenery nobody wants to model: the fence, the tables, the
pedestal under the robot, the racks, the conveyor's body, the pallets. bt.parts
generates them from parameters — as ordinary residents (boxes under a name
prefix, a frame where the next thing mounts, a device or a sensor where one
belongs) with their part identity already pinned, so the
BOM counts them and the layout sheet labels each
assembly once. Change a parameter and the geometry, the BOM line and the
drawing change together.
fence = bt.parts.fence(scene, "fence", path=[(-2, -2), (2, -2), (2, 2), (-2, 2)],
height=2.0, panel_pitch=1.0, door=(0, 2),
model="ST20", manufacturer="TROAX", mass_kg=12)
ped = bt.parts.pedestal(scene, "pedestal", height=0.5, position=(0, 0), model="PD-500")
scene.set_robot_base_pose(*scene.frame("pedestal/mount"))
conv = bt.parts.conveyor(scene, "conv", length=2.0, width=0.4, position=(0, 1.2, 0.7),
direction=(1, 0), speed=0.2, model="GVL-2000")
bt.parts.table(scene, "table", size=(1.2, 0.8, 0.75), position=(1.0, 0.0), model="HFS8-1200")
bt.parts.pallet(scene, "pallet", position=(-1.2, 0.0))
rack = bt.parts.rack(scene, "rack", size=(1.2, 0.6, 1.8), position=(-1.2, 1.2), levels=4)
bt.parts.light_curtain(scene, "lc", frm=(-1, -2), to=(1, -2), model="SL-V")
bt.parts.photoelectric(scene, "eye", frm=(0.0, 1.0, 0.75), to=(0.0, 1.4, 0.75),
watch=["part"], model="E3Z-D62")
What each generator makes¶
| generator | obstacles | frame | device / sensor | parts pinned |
|---|---|---|---|---|
fence |
panels under <name>/panels/, posts under <name>/posts/, the door as <name>/door |
— | — | <name> (structure.fence, qty = panels), <name>/posts (structure.fence.post, qty = posts), the door (structure.door) |
table |
<name>/top, four legs |
<name>/top (centre of the top face) |
— | <name> (structure.table), and with a catalog <name>/top (the board, where the maker sells it as its own article) |
pedestal |
<name>/base, <name>/column, <name>/top |
<name>/mount (the robot's base pose) |
— | <name> (structure.pedestal) |
conveyor |
<name>/belt, side rails, legs |
<name>/infeed, <name>/outfeed |
the conveyor device <name>, its zone on the belt |
the device (conveyor) — the body is its geometry, not a second product |
rack |
four uprights under <name>/uprights/, a board per level under <name>/shelves/ |
<name>/level0 … upwards (the centre of each deck) |
— | <name> (structure.rack), and with a catalog <name>/shelves (structure.rack.shelf, qty = levels) |
cabinet |
<name>/body, its plinth as <name>/base, the mounting plate standing inside as <name>/plate |
<name>/front (centre of the door face at floor level — where an operator stands) |
— | <name> (structure.cabinet), and with a catalog <name>/base and <name>/plate (structure.cabinet.base / .plate — the plinth and the plate are articles of their own) |
pallet |
bottom boards, blocks, deck boards | <name>/top |
— | <name> (pallet, EPAL 1 by default) |
light_curtain |
two columns, <name>/column_a|b |
— | the beam sensor <name>, spanning the gap between the lens faces (trips on anything in the field) |
the sensor (sensor.light_curtain) — with a catalog the emitter/receiver pair's model number, and the range of the resolution chosen |
photoelectric |
the sensor body <name>/body behind its lens; a through-beam pair adds <name>/receiver, a retroreflective one <name>/reflector |
— | the beam sensor <name> (trips on watch, and on the robot if asked) |
the sensor (sensor.photoelectric), and with a catalog the reflector where the maker sells it separately |
proximity |
the threaded barrel <name>/body behind the sensing face |
— | the beam sensor <name>, as long as the switch's sensing range (a few millimetres) |
the sensor (sensor.proximity) — with a catalog the model of the size, shield and output chosen, and its range |
power_supply |
the box <name>/body on its rail |
— | — | <name> (power_supply, carrying output_v / output_a — what the cell's current_a total is checked against) |
remote_io |
the coupler <name>/coupler, a box per terminal unit <name>/di{i} / <name>/do{i} |
— | the I/O node <name> (remote_io, a channel per point, hung off uplink=) |
the coupler (io.remote, with di / do counts), and every unit as a line of its own |
wall |
a pier per solid stretch under <name>/e{edge}_{i}, the wall over each opening under <name>/head/, a column at each shared corner |
<name>/opening{edge}_{i} (on the floor at each doorway, facing along the wall) |
— | <name> (structure.wall, carrying the run's length, height and thickness) |
machine_tool |
the enclosure under <name>/shell/, <name>/bed, <name>/column, <name>/saddle, <name>/table, <name>/head, the door leaves under <name>/side_door/ and <name>/front_door/, the panel's plate |
<name>/table, <name>/entry, <name>/door/side/handle, the panel's |
the side door as a linear axis <name>/side_door (servo / air) with the stops closed / open as lanes, a zone per button |
<name> (machine_tool.vmc), <name>/side_door (machine_tool.door, drive and stroke), the panel and its buttons — with a catalog the pack's envelope, options, door times and interface |
operator_panel |
<name>/plate, a cap per button |
<name>, <name>/<button>, <name>/<button>/press (+Z into the panel) |
a zone sensor <name>/<button> inside each cap, as deep as the stroke |
<name> (hmi.panel), each button (hmi.button, head, travel, force) — with a catalog the box by its positions, the buttons and the E-stop by article |
vise |
<name>/body, <name>/jaw_fixed, <name>/jaw_moving |
<name>/jaw (the jaw floor between the jaws) |
— | <name> (fixture.vise, jaw width and opening) — with a catalog the jaw width matched against the ones sold, the maximum opening from the pack |
lathe |
the enclosure under <name>/shell/, <name>/bed, <name>/rear, <name>/headstock, <name>/turret (and <name>/tailstock), the front door leaf under <name>/front_door/, the panel's plate |
<name>/spindle (the nose, +Z along the axis toward the tailstock), <name>/entry, <name>/door/front/handle, the panel's |
the front door as a linear axis <name>/front_door (servo / air) with the stops closed / open, or a loose leaf with two limit switches; a zone per button |
<name> (machine_tool.lathe), <name>/front_door (machine_tool.door, drive and stroke), the panel and its buttons |
chuck |
<name>/body, <name>/jaw0 … (proud of the face around the gripping diameter) |
<name>/face (the face centre, +Z out along the spindle axis — a load comes in along -Z) |
— | <name> (fixture.chuck, diameter, opening and jaw count) — with a catalog the diameter matched against the ones sold, the maximum opening from the pack |
stairs |
a walkable checker-plate tread per step under <name>/tread…, a plate stringer and support leg each side, the handrail under <name>/handrails/ |
<name>/foot, <name>/top (author the vehicle path's z between them) |
— | <name> (structure.stairs), and with a catalog <name>/handrails (structure.stairs.rail, qty = 2 sides) |
Every generator takes model=, manufacturer= and free attributes
(mass_kg=…) for the part it pins — or catalog=, the id of a spec pack, and
then the dimensions, part numbers and mass come from the catalog and the
generator refuses a size nobody sells (see
the model catalog). Each returns a
Built naming what it made — built.frames,
built.devices, built.obstacles — with built.remove(scene) to take the
whole thing down again.
Drawn, and what it hits¶
A catalog part is drawn the way it looks: a mesh panel as a tube frame with a
grid of wire in it, a conveyor with its rollers and drive, a rack with its
beams and braces. All of that is decoration — added with collision off,
under <name>/trim/ — while the massing underneath (the panel slab, the belt,
the uprights) keeps collision and stops being drawn where the detail stands in
for it. Changing detail therefore never changes what a robot can hit, what
the BOM says, or what a plan gives you; it costs scene entries and nothing
else. detail="plain" is the bare massing, and is what a generator called
without a catalog does — there are no real sections to draw from.
examples/engineering/equipment_cell_demo.py
builds a cell whose fence, conveyor and rack all come from the catalog, and
prints the bill it can be ordered from;
examples/basics/demo.py
equips the tutorial cell the same way, next to a USD layer that keeps the
layout and the teach frames.
Where the drawing comes from is the product's business, not the generator's: a
pack can name a file of primitives per part (components[].trim, a URDF or
xacro), and the generator expands it to the size at hand with
load_urdf instead of drawing its own
shapes. One parametric file covers every size a product is sold in, so making
a fence look like that maker's fence is an edit to the catalog, not to
botrail. Parts the pack says nothing about keep the built-in look.
A fence's edge is split into panels of about panel_pitch (stretched so an
edge takes a whole number of them) with a post at every corner and between
panels; door=(edge, panel) makes that panel the door. Because the counts
come from the geometry, panel_pitch=0.5 on the same path is twice the
panels on the BOM and twice the panels on the sheet — the same edit, seen
by every document.
Names live under the generator's name, so a device and its body share
one name (conv the device, conv/belt the slab): scene.set_part("conv",
…) then needs kind="device" to say which — the generators do this for
you.
Bringing shapes from CAD — the Geometry Provider pattern¶
botrail does not model shapes, and will not: no sketches, no features, no CAD kernel. A fence is panels of a pitch along a path and a table is a top on legs — the meaning is what a cell verifies and what its documents need, and the few centimetres a real profile differs by change nothing the verifier measures.
Anything with a shape of its own comes in from the tool that owns it. The pattern is always the same three steps:
- Generate or export the shape elsewhere — CadQuery, FreeCAD, Blender, the vendor's CAD — as a mesh (OBJ / STL) or a USD stage.
- Load it as geometry —
scene.add_mesh(name, path, position=…)for one body,scene.load_usd(stage, prefix=…)for an assembly (its prims become obstacles under the prefix, its Xforms named frames). - Pin what it is —
scene.set_part(name, manufacturer=…, model=…, catalog=…), on the mesh or on the group the stage came in as.
# CadQuery makes the bracket; botrail places it and knows what it is.
import cadquery as cq
bracket = cq.Workplane("XY").box(0.12, 0.08, 0.01).faces(">Z").hole(0.008)
cq.exporters.export(bracket, "bracket.obj")
scene.add_mesh("bracket", "bracket.obj", position=(0.4, 0.2, 0.75))
scene.set_part("bracket", model="BR-120", manufacturer="ACME", mass_kg=0.35)
The mesh collides (a cached convex decomposition), draws in the studio with
its mtllib colours, projects onto the layout sheet as its bounding
rectangle, and lands on the BOM as one identified line — exactly like a
generated part. What botrail keeps is the meaning (this is a bracket,
model BR-120, one of them, 0.35 kg, standing there); the shape stays the
provider's business, and can be regenerated there without the cell
noticing anything but the geometry.
The model catalog is the same pattern with
the identity already attached: Robot.from_catalog (and
catalog_package() for non-robot packages) brings the maker's mesh and
its manifest, so the BOM line writes itself.