Skip to content
FlywheelCAD
Manual pages

Reference

Python Syntax

Every public call in the FlywheelCAD 0.35 Python API, with its exact signature.

A FlywheelCAD document is ordinary Python. The app runs it with the bundled flywheelcad module, whose calls emit CAD commands that the app then parses and replays. Every script starts the same way:

# flywheelcad-format: 1
from flywheelcad import *
cad = FlywheelCAD()

The signatures below are copied from flywheelcad.py, with two keyword arguments left out to keep them readable: almost every call that creates something also accepts name= (otherwise the name comes from the variable you assign the result to), and every call that creates a body also accepts quality= (a per-body mesh quality). name= is shown only where it matters. Items marked 0.35 changed or arrived after version 0.25.

Working with an AI coding assistant? The scripting guide carries the same API plus the rules and worked patterns in one Markdown file you can drop into the assistant's context.

Download the AI scripting guide (.md)

Quick start

A plate driven by variables, with a hole whose size follows one of them, built with a helper module that sits next to the main script. Save the two files together — inside a .fwcad bundle, or as a flat .py with the helper beside it:

Plate.fwcad/
  main.py      # the script
  shapes.py    # a helper module, imported by name
# main.py
# flywheelcad-format: 1
from flywheelcad import *
cad = FlywheelCAD()

from shapes import rectangle              # shapes.py sits next to this file

WIDTH = cad.variable(80, driving=True)    # editable in the Variables panel
HEIGHT = cad.variable(50, driving=True)
HOLE_DIA = cad.variable(8, driving=True)
INSET = cad.variable(12, fixed=True)      # a constant

cad.with_sketch("xy")
outline = rectangle(WIDTH, HEIGHT)
bottom, right, top, left = outline

centre = cad.point2d(INSET, INSET)        # starts at (12, 12); no constraint added
hole = cad.circle2d(centre, HOLE_DIA / 2)  # radius 4, plus a linked radius dimension
cad.point_line_distance(point=centre, line=bottom, distance=INSET)
cad.point_line_distance(point=centre, line=left, distance=INSET)

plate = cad.extrude("xy", region(loop=outline, holes=[[hole]],
                                 inside=(WIDTH * 0.75, HEIGHT / 2)), 6)
cad.set_color(plate, "steel", finish="metallic")
# shapes.py
from flywheelcad import *
cad = FlywheelCAD()                       # the same shared object main.py uses


def rectangle(width, height, plane="xy"):
    """A constrained width x height rectangle, lower-left corner on the origin."""
    bl = cad.point2d(0, 0)
    br = cad.point2d(width, 0)            # a variable seeds the point; no constraint added
    tr = cad.point2d(width, height)
    tl = cad.point2d(0, height)
    bottom = cad.line2d(bl, br)
    right = cad.line2d(br, tr)
    top = cad.line2d(tr, tl)
    left = cad.line2d(tl, bl)
    cad.horizontal(line=bottom)
    cad.horizontal(line=top)
    cad.vertical(line=right)
    cad.vertical(line=left)
    cad.coincident(p0=bl, p1=cad.origin(plane))
    cad.length(line=bottom, length=width)  # dimensions keep the variables live
    cad.length(line=right, length=height)
    return [bottom, right, top, left]

Change WIDTH, HEIGHT or HOLE_DIA in the Variables panel and the plate re-solves, because the lengths and the hole radius are dimensions. The point coordinates and the inside= point were only computed from the variables when the script ran; the geometry follows because the dimensions move it, not because the points remember the variables. The sketch is fully constrained: the corner sits on the origin, and the hole centre is held INSET from two edges.

Variables and expressions

A cad.variable, or arithmetic on variables, can go anywhere a number is expected. Only a dimension stores it as a live expression; everywhere else it is resolved to its number when the script runs. The long form, with more examples, is in the user manual: §3.4, Variables outside dimensions.

Dimensions: cad.length, cad.distance, cad.radius, cad.angle, cad.point_line_distance, and cad.extrude(distance=…, offset=…) Live. The document keeps the expression, so editing a driving variable re-solves the sketch or rebuilds the body. Expressions must be linear: + - * /, where a variable may be multiplied or divided by a number but not by another variable. offset= stays live like distance.0.35
cad.point2d(x, y), cad.point(x, y, z) Resolved to numbers. For point2d the number is only the solver's starting guess — no constraint is added. Dimension the point if it must follow the variable.0.35
cad.circle2d(center, radius) The radius is resolved and a cad.radius(circle=…, radius=…) dimension carrying the expression is added right after the circle, so editing the variable resizes it. That dimension has no line of its own in the script, so change the variable rather than editing the dimension in the sketch. A non-linear radius such as w * h or a / b still builds the right circle, with a warning, but keeps no link.0.35
region(inside=(x, y)) Each coordinate is resolved. An inside point has no link to lose.0.35
cad.move, cad.rotate, cad.scale, cad.offset, cad.pattern_linear, cad.pattern_circular, cad.instance, cad.mate_coincident, cad.mate_align, polar(), ring(), grid() Coordinates, angles, factors, spacings and offsets are resolved to numbers. The document records the number, so editing the variable afterwards does not move the body. Counts (count=, ring(count), grid(nx, ny)) must be plain integers.0.35
cad.text(size=, angle=, distance=, at=, direction=), cad.helix(start_angle=) Resolved the same way: the value is recorded, not linked.0.35
cad.extrude(twist=, draft=, edge_radius=), cad.sweep(twist=, edge_radius=), a boolean's blend radius= Plain numbers only. A variable is refused with an error naming the argument; a Python variable that holds a number is fine.0.35
a ** 2 Computed at script time into a plain number, because the document's expression grammar has no exponent. The link is lost, and both operands need values.0.35
cad.variable() with no value= Only the solver knows its value, so it cannot be used where a number is resolved; the error names the argument and says why. It works in dimensions.
What counts as a number int, float, Decimal, Fraction and numpy scalars (converted to a double). Strings — even "12" — bytes, bool and complex are refused wherever a number is expected; convert with float(...) yourself.0.35
w = cad.variable(80, driving=True)
hole = cad.variable(9, driving=True)
depth = cad.variable(12, driving=True)

cad.with_sketch("xy")
c = cad.point2d(w * 0.35, 25)            # x = 28: a starting guess, no constraint added
h1 = cad.circle2d(c, hole / 2)           # radius 4.5, plus a linked cad.radius dimension
peg = cad.extrude("xy", region(loop=[h1], inside=(w * 0.35, 25)), depth)   # distance stays live
row = cad.pattern_linear(peg, count=3, spacing=w / 4)   # spacing is recorded as 20

A variable is a symbol, not a number the script can read: w.value raises an error that names the forms above. And a plain cad.variable(40) — neither fixed nor driving — is a free unknown the solver may move; a coordinate taken from it keeps the value it had at that line. Declare values you use as coordinates fixed=True or driving=True.

Setup and planes

FlywheelCAD() Returns the shared API object. Every call returns the same object, so helper modules share one naming counter. It also puts the plane origins origin_xy, origin_yz and origin_zx into the calling module's globals. Bind it to the name cad at module level: the app inserts document settings such as mesh quality right after that line.
cad.with_sketch(plane_name) Makes a plane the current sketch: "xy", "yz", "zx" or a custom plane's name. Later 2D geometry lands on it. Also puts origin_<plane_name> into your globals.
cad.origin(plane_name) Returns a plane's origin point (the same point as origin_xy and so on), for pinning geometry to it.
cad.point(x, y, z) A fixed 3D reference point in world coordinates, for custom planes, anchors and 3D sweep paths. A coordinate may be a variable or expression; it is resolved to its number.0.35
cad.create_sketch_plane(p1, p2, p3, name=None) A sketch plane through three 3D points: origin at p1, normal (p2 − p1) × (p3 − p1). The points can be cad.point(...) refs, sketch points or body topology points such as box.v0. Always pass name=: without it planes are numbered custom1, custom2, … in creation order, so inserting one renumbers the rest.

Sketch axes: on "xy" sketch x is +X, sketch y is +Y and an extrude goes +Z; on "yz" they are +Y, +Z and +X; on "zx" they are −X, +Z and +Y.

Sketch geometry

cad.point2d(x, y) A 2D point in the current sketch's coordinates. A variable or expression is resolved to its number, which is only the solver's starting guess; no constraint is added.0.35
cad.line2d(p1, p2, construction=False) A line from p1 to p2. construction=True (on any 2D primitive) makes guide geometry: drawn dashed and ignored by region detection, so it can never be part of a region loop.
cad.circle2d(center, radius, construction=False) A circle about a center point. A variable or expression for radius is resolved, and a linked cad.radius dimension carrying it is added after the circle (see Variables and expressions).0.35
cad.arc2d(center, start, end, clockwise=False, construction=False) An arc about center from start to end; both end points must lie on one circle. Counter-clockwise unless clockwise=True.
cad.ellipse2d(focus1, focus2, point, construction=False) An ellipse from its two foci and one point on its rim.
cad.spline2d(points, closed=False, construction=False) A cubic spline through a list of points. closed=True joins the last point back to the first.
cad.fillet(line1, line2, radius) Rounds the corner where two elements — lines or arcs, in any mix — share an end point: both retract to the tangent points and a tangent arc bridges them, with tangency and radius constrained so the fillet survives re-solves. Returns the arc; list it between the two in a region loop, region(loop=[l1, f1, l2, ...]). Menu: Modify → Fillet Corner…
cad.trim(element, near) Trims element in place at the intersection nearest near=(x, y). Returns (element, split); split (named <element>_1) exists only if the trim cut the element in two. Menu: Modify → Trim (T).
cad.mirror_elements(elements, symmetry_line) Mirrors a list of elements across a line. Copies are named <source>_m (_m2, _m3, … for repeat mirrors); reach them with ref("l1_m"). Menu: Modify → Mirror (M).
cad.merge_points(source, target) Welds source into target; afterwards both names refer to one point. Menu: Modify → Merge Points.

There is no cad.* pattern call for sketch geometry. Modify → Pattern… writes an ordinary for loop instead — one construction line plus an equal_lines and an angle per copy, driven by a cad.variable step — so the copies stay linked to the source. The recipe is under Placement helpers and in the manual's §3.5.

Geometric constraints

Constraints relate existing elements. They take no value, and all but the two tangent-line constraints return nothing. The keyword form — cad.horizontal(line=l1) — is what the app writes when you add a constraint in the GUI; the positional form is also accepted. Wherever a constraint or dimension expects a point, you may pass a circle or arc and it means its centre.0.35

cad.horizontal(line)Makes a line horizontal in the sketch.
cad.vertical(line)Makes a line vertical in the sketch.
cad.parallel(line1, line2)Keeps two lines parallel.
cad.perpendicular(line1, line2)Keeps two lines at 90°.
cad.collinear(line1, line2)Puts two lines on the same infinite line.
cad.coincident(p0, p1)Makes two points one point by merging p0 into p1 — the same command as cad.merge_points(source=p0, target=p1), not an extra equation.
cad.point_on_line(point, line)Holds a point on a line.
cad.point_on_circle(point, circle)Holds a point on a circle.
cad.point_on_ellipse(point, ellipse)Holds a point on an ellipse.
cad.point_on_plane(point, plane)Holds a 3D point on a plane, e.g. cad.point_on_plane(point=p1, plane="xy").
cad.fixed(element)Pins an element's own position — point, line, circle, arc, ellipse or spline — at its current values. It counts toward the degrees-of-freedom badge and does not override a conflicting constraint (the badge then reads conflicting). An arc pins its centre and radius, not its end points.
cad.concentric(circle1, circle2)Gives two circles one centre.
cad.equal_lines(line1, line2)Gives two lines equal length.
cad.equal_radius(circle1, circle2)Gives two circles equal radius.
cad.circle_tangent_line(circle, line, anchor=None, name=None)Makes a circle tangent to a line and returns the tangency point. Assign the result (tp = cad.circle_tangent_line(circle=c1, line=l1)) or pass name=; otherwise it raises ValueError. anchor=(x, y) picks which of two tangent points the solver goes to.
cad.circle_tangent_circle(circle1, circle2)Makes two circles tangent.
cad.ellipse_tangent_line(ellipse, line, anchor=None, name=None)Makes an ellipse tangent to a line and returns the tangency point; the same assignment rule applies.
cad.spline_tangent_line(spline, line, control_point=None, control_point_index=None)Makes a spline tangent to a line at one control point, chosen by point ref (control_point=) or position (control_point_index=). Omit both to let the app choose.
cad.symmetry_line(p1, p2, symmetry_line)Makes two points mirror images about a line.
cad.symmetry_plane(point1, point2, plane)Makes two points mirror images about a plane.

Dimensions and variables

A dimension pins a value: a number, a cad.variable, or a linear expression of variables. Dimensions are the only calls that keep an expression live. A dimension written at module level can be edited in the sketch (double-click it); one bound to an expression or to a plain Python variable is left as written by that editor.

cad.variable(value=None, fixed=False, driving=False) Declares a named scalar (named after the variable you assign it to). fixed=True: a constant that never moves. driving=True: held while solving but editable in the Variables panel or with cad.update. Neither: a free unknown the solver may move. Passing both raises ValueError. value must be a number: Decimal, Fraction and numpy scalars are converted, and a string such as "12" is refused.0.35
cad.length(line, length)A line's length, e.g. cad.length(line=l1, length=w).
cad.distance(p0, p1, distance)Straight-line distance between two points. To hold a distance from an axis, use cad.point_line_distance.
cad.radius(circle, radius)A circle's radius, e.g. cad.radius(circle=c1, radius=8).
cad.angle(line1, line2, angle=None)The angle between two lines, in degrees. The signature defaults angle to None, but a run without it fails (“Missing dimension value parameter”), so always pass it.
cad.point_line_distance(point, line, distance)Perpendicular distance from a point to a line.
cad.fraction(refA, refB, multiplier) Keeps one variable a multiple of another, refA = multiplier × refB, e.g. cad.fraction(refA=chord, refB=span, multiplier=0.25). Works since 0.35; earlier builds failed on every run.0.35
cad.update(changes)Changes values after the fact and re-solves: cad.update({w: {"value": 60}}) for a variable, cad.update({p1: {"x": 10, "y": 20}}) for a point.
cad.ensure_convergence()Solves the current sketch now, so the script continues from settled positions.
cad.label_offset(key, dx, dy)Records where a dimension label was dragged in the sketch (key such as "length:l1"). The app writes these lines and they carry no geometry; leave existing ones alone and don't write new ones.
w = cad.variable(80, fixed=True)
clearance = cad.variable(12, driving=True)
cad.length(line=l1, length=w * 2 + clearance)

Regions

region(loop=None, holes=None, inside=None, name=None)
cad.region(loop=None, holes=None, inside=None, name=None)
The closed area a body is built from; the free function and the method are identical. loop (required) lists the outer boundary's elements in traversal order; holes is a list of hole loops, each its own list; inside=(x, y) is a point in the material, in sketch coordinates, that picks between areas tracing the same loop. Every closed loop drawn inside loop on the same sketch must be listed under holes= (or claimed as material by an inside= point in it), or the region is refused with those loops named. inside= coordinates may be variables, resolved to numbers; name= is a label only — a region is not a named element, so it creates nothing (a warning says so).0.35
rev(element) Marks a loop element as traversed against its own direction (lines run p1 → p2, circles and arcs counter-clockwise, splines in point order). It is written as -l4. Also used in cad.path(chain=...).
ref(name) A reference to an element by its script name, for names without a Python variable — a mirrored copy, for example: ref("l1_m"). A name that is not a valid identifier raises ValueError.

Bodies from sketches

Each call returns a body ref (for example body1) and also accepts export=: False leaves the body out of mesh export. It must be a real bool.

cad.extrude(plane, region, distance, direction=None, export=None, edge_radius=None, twist=None, twist_center=None, offset=None, draft=None) Extrudes a region along the plane normal. direction: None or "positive" (along the normal), "negative", or "symmetric" (both ways). offset shifts the start off the plane (negative is allowed). draft tapers the walls by that many degrees, measured from the sketch-side face (positive slopes outward). twist turns the profile by that many degrees over the full height, about twist_center (default: the region centroid). edge_radius rounds every edge of the body. distance and offset take a number, variable or expression and stay live; edge_radius, twist and draft take plain numbers only.0.35
cad.revolve(plane, region, axis_line=None, angle=None, export=None, edge_radius=None, **kwargs) Revolves a region about a line in the same sketch by angle degrees (360 for a full solid). axis_line and angle are both required; the None defaults and **kwargs exist only to refuse the removed axis= spelling with a clear message.
cad.loft(start_plane, start_region, end_plane=None, end_region=None, export=None, edge_radius=None) A solid between two profiles on two planes, which need not be parallel. end_plane and end_region are required. Write all four as keywords (start_plane=…): a positional call runs, but the Body Inspector then cannot edit that body's quality. Either region may also be a list of regions, lofted as one profile.
cad.multi_loft(planes, regions, export=None, edge_radius=None) One smooth solid through two or more sections — a list of planes and a list of regions of the same length — with no internal seams. The surface is interpolated, so it can bulge between sections; for straight-walled round stacks use one revolve.
Body refs: extrude and revolve bodies expose their far-face vertices as attributes — box.v0, box.v1, …; hole vertices as box.h0_v0; a symmetric extrude as box.pos_v0 / box.neg_v0. Pass them to cad.create_sketch_plane(...) or cad.project_point(...). A mistyped attribute is not caught (box.v9999 returns a ref that fails later), so when unsure build reference geometry with cad.point(x, y, z).

Sweeps, paths and text

cad.helix(radius, pitch, turns, axis_origin=(0, 0, 0), axis_dir=(0, 0, 1), start_angle=0.0, left_handed=False) A helical path — a path handle, not a body: coil radius, rise per turn and number of turns (may be fractional), about an axis. start_angle is in degrees; a variable there is resolved to its number.0.35
cad.path(plane, chain) An open path on a sketch plane from lines, arcs and splines joined end to end, e.g. cad.path("xy", chain=[l1, rev(a2), s3]). It must not close, and a full circle, ellipse or closed spline cannot be in it. Keep it clear of the profile's own elements.
cad.path3d(points, closed=False, up=None) A smooth 3D path through named 3D points (cad.point(...) refs, not tuples). up=(x, y, z) sets the profile's starting orientation. closed=True is not supported yet and raises.
cad.sweep(plane=None, region=None, path=None, sections=None, twist=0.0, export=None, edge_radius=None) Sweeps a profile along a path. Pass either plane + region or sections=[...], not both. The profile's 2D coordinates are used unchanged as its offset from the path, so draw it centred on the sketch origin — the coil radius belongs to the helix, not the profile. twist is total degrees over the path; twist and edge_radius take plain numbers only.0.35
sweep_section(fraction, plane, region) One section for cad.sweep(sections=[...]): a profile at an arc-length fraction along the path. Fractions must rise strictly from exactly 0.0 to exactly 1.0; holes are not supported in this form; two sections that differ only in size must sit on different planes.
cad.text(string, size=10.0, font="Helvetica", at=(0.0, 0.0), angle=0.0, direction=None, distance=None, align="left", export=None, plane=None) Writes string with a system font. size is the cap height in mm, at the baseline origin, align "left", "center" or "right"; angle (degrees) or direction=(dx, dy) turns the run. Without distance the glyphs become fixed spline outlines in the sketch; with distance= each glyph becomes its own extruded body (not unioned). plane= builds on another plane without switching the current sketch. Returns one ref per character. size, angle, distance, at and direction accept variables, resolved to numbers.0.35
cad.with_sketch("xy")
wire = cad.circle2d(cad.point2d(0, 0), 1.2)          # profile at the sketch origin
coil = cad.helix(radius=8, pitch=4, turns=5)         # the coil radius lives on the path
spring = cad.sweep("xy", region(loop=[wire]), coil)

Booleans, transforms and patterns

Booleans consume their inputs: the operands are hidden and so leave the export. Transforms and patterns keep the source and return new bodies. Coordinates, angles, factors and spacings in transforms and patterns accept variables, resolved to numbers — editing the variable later does not move the body.0.35

cad.bool_union(*bodies, export=None, blend=None, radius=None)The union of two or more bodies. blend="smooth" (a rounded seam) or "chamfer" (a 45° bevel) needs exactly two bodies and a radius greater than 0; "sharp" is the default. name= may not repeat an operand's name.
cad.bool_difference(*bodies, export=None, blend=None, radius=None)The first body minus all the others.
cad.bool_intersection(*bodies, export=None, blend=None, radius=None)Only the volume common to all the bodies.
cad.offset(body, distance=None)A new body grown (positive) or shrunk (negative) by distance in every direction; distance is required. No export=. Shell a body by differencing a shrunk copy from it.
cad.move(body, *args, translate=None, export=None, **kwargs)A moved copy: cad.move(b, translate=(10, 0, 0)). Pass translate by keyword; *args and **kwargs exist only to refuse the removed cad.move(b, dx, dy, dz) and dx=/dy=/dz= forms by name.
cad.rotate(body, axis=(0, 0, 1), angle=0, center=(0, 0, 0), export=None)A copy rotated by angle degrees about axis through center.
cad.scale(body, factor=None, sx=None, sy=None, sz=None, export=None)A scaled copy: uniform factor, or per axis with sx/sy/sz. factor wins if both are given.
cad.mirror_body(body, plane="yz", export=None)A copy mirrored across a standard or custom plane.
cad.copy(body, export=None)An identical copy in place.
cad.pattern_linear(body, direction=(1, 0, 0), count=2, spacing=10, export=None, name=None)Copies along direction (normalised), spacing mm apart. count includes the seed and runs 2–1000. Returns [seed, copy_1, …]; the copies are named <name>_1, <name>_2, … after the variable you assign to or name= — one of the two is required.
cad.pattern_circular(body, axis=(0, 0, 1), count=4, angle=360, center=(0, 0, 0), export=None, name=None)Copies around axis through center. angle is the total sweep, up to ±360: a full circle steps by angle / count, a partial fan by angle / (count − 1) so a copy sits at each end.
cad.delete_body(body)Deletes a body, and every boolean or offset body built from it.

Components, instances and mates

Components define a reusable part once and place it many times. A component body is written flat inside a with cad.component(...) block — blocks never nest; larger structures nest by instancing other components. A component that is never instanced is transparent: it builds the top-level design, so a component file opened directly behaves like a normal editable part.

with cad.component("bracket"):
    cad.with_sketch("xy")
    # ... sketch + region r ...
    body = cad.extrude("xy", r, 5)
    cad.set_color(body, "steel", finish="metallic")
    cad.component_export(body)                                # assembly-visible part
    cad.component_export_point("hole", cad.point(10, 0, 0))   # named anchor for mating

b1 = cad.instance("bracket")
b2 = cad.instance("bracket", translate=(50, 0, 0), mirror_plane="zx")
cad.mate_coincident(b2, "hole", b1.hole, offset=(0, 0, 5))
cad.component(name, label=None)Use as with cad.component("bracket"):. Everything inside builds the component's own sub-design. With as wing, wing.component_export(...) works as well. label= is a display name for the UI.
cad.parametric(prefix, build, *, label=None, **params)Builds one component per distinct parameter set from a zero-argument build() and returns its name for cad.instance; the same parameters reuse the build. label= is keyword-only, never passed to build, and not part of the memo key.
cad.component_name(prefix, *parts)The deterministic, identifier-safe name parametric uses for a parameter set.
cad.has_component(name)True if that component was already emitted in this run.
cad.component_export(body, name=None)Marks a body as a visible part of the component. With several exports, each surfaces on an instance as <instance>_<name>; name= sets that label.
cad.component_export_point(name, point)Promotes a point to a named anchor. Each instance exposes it as <instance>.<name> (e.g. b1.hole) for mates and assembly geometry.
cad.instance(component, translate=None, mirror_plane=None, scale=None, axis=None, angle=None, center=None, export=None, **kwargs)Places a component. Transforms apply in a fixed order: scale, rotate (angle about axis through center), mirror, translate. Returns a body ref usable anywhere a body is. export=False leaves every part out of export; quality= is accepted but ignored with a warning (set quality inside the component). **kwargs exists only to refuse the removed mirror= spelling. Placement values accept variables, resolved to numbers.0.35
cad.mate_coincident(instance, anchor, target, offset=None)Snap: moves the instance so its anchor sits on target — another instance's anchor, a topology point or a sketch point — plus an optional (dx, dy, dz) offset. Position only. offset accepts variables.0.35
cad.mate_align(instance, anchor1, anchor2, target1, target2, offset=None)Snap and orient: anchor1 onto target1 (plus offset), and the direction anchor1 → anchor2 turned parallel to target1 → target2.
cad.mate(instance, anchor, target)Solved mate. Several cad.mate calls on one instance are solved together as one rigid pose — three point mates fix position and orientation. The target never moves. No offset=.
cad.mate_axis(instance, anchor1, anchor2, target1, target2)Solved: holds the instance axis anchor1 → anchor2 collinear with target1 → target2, leaving slide along it and spin about it free (a cad.mate onto a point on the axis pins them). No offset=.
Refused mates: the run fails, naming the rule, if a solved mate (cad.mate or cad.mate_axis) is used on an instance placed with a rotation, mirror or non-unit scale, is combined with a snap mate on the same instance, is chained onto another solved-mated instance, targets the instance's own anchor, or sits inside a component block. Details: manual §7.4.
Multi-file assemblies: component files live next to the assembly file and are pulled in with plain import fuselage — the import defines the component, the assembly places instances and mates them. Put those imports below cad = FlywheelCAD(), because a component module emits its geometry when it is imported. The Glider and AirplaneNested projects in the samples download are complete worked examples, including nested sub-assemblies that re-export parts and anchors of their own children.

Appearance, quality and export

set_color, set_export, set_cell_size, hide and show accept a body ref or the body's name as a string. The string form is the only way to reach the parts of an instance (e.g. "motor_1_can") or a body whose variable was reused in a loop.

cad.set_color(body, color, finish=None)
body.set_color(color, finish=None)
Display colour: "#RRGGBB", "#RRGGBBAA" (alpha is translucency), "#RGB", a name such as "red" or "steel", or an (r, g, b[, a]) tuple (0–1 or 0–255). finish is "matte", "glossy" (the default), "metallic" or "glass". Colours are written into 3MF and OBJ export, and glTF/GLB export carries colour and finish as a PBR material; STL is geometry only. A colour set inside a component applies to every instance.0.35
cad.set_export(body, value=True)
body.set_export(value=True)
Includes or excludes a body from mesh export, in every format. value must be True or False. It applies to visible bodies: a hidden body is never exported, and a flag set on it takes effect once it is shown.0.35
cad.hide(*bodies) Hides one or more bodies; saved with the document. Hidden bodies are left out of the export.0.35 A boolean already hides the operands it consumes, so don't hide those again. At least one body is required.
cad.show(*bodies)Shows hidden bodies again. Placed after a boolean, it brings a consumed operand back into view and into the export.
cad.set_cell_size(body, cell_size)
body.set_cell_size(cell_size)
An absolute meshing cell size in mm for one body, for small features on a large sampled body (booleans, lofts, rounded or drafted extrudes). No effect on bodies that mesh exactly, such as plain extrudes and revolves. The minimum is 1e-6.
cad.set_quality(quality) The document's mesh quality: "preview", "standard" (the default), "high", "ultra" or "precision". It must come before the first body, at most once, and never inside a component block. A per-body quality= overrides it. View → Mesh Quality writes this line.
cad.set_units(units) The document's presentation unit: "mm", "cm" or "in". It changes GUI entry and readouts and 3MF export only; the numbers in the script stay millimetres. View → Document Units writes this line.
cad.set_default_cell_size(cell_size) A document-wide default cell size in mm for bodies without their own. It raises every body's cell budget, so it is expensive. It must come before the first body. View → Default Cell Size… writes this line.

The last three are the user's controls: a script written for someone else should leave them out. Mesh export is on the Body menu — Export to 3MF…, STL…, OBJ…, GLB… and glTF… — and sketches export from File → Export Sketch as DXF… or SVG…. The manual's §8.7 lists what each format carries.

Section and projection

cad.section(body, plane)The cross-section where a plane cuts a body, drawn on that plane's sketch. An extrude (or a boolean, transform or instance of one) cut parallel to its profile gives true lines and arcs; anything else gives a sampled spline. Not the same as sweep_section(...).
cad.project(body, plane)Projects a body's edges onto a plane, drawing-style, with no hidden-line removal. True lines and arcs when viewed along an extrude's axis; otherwise a sampled silhouette spline.
cad.project_point(point, plane)Projects one 3D point — a topology point such as box.v0, or a cad.point — onto a plane.

All three are snapshots: they update only when the whole script re-runs.

Placement helpers

Free functions, not cad. methods. They compute numbers and emit nothing; use them to place copies in a loop. Each scalar accepts a variable, resolved to its number; counts must be plain integers.0.35

polar(radius, angle, center=(0.0, 0.0))The (x, y) at radius and angle degrees around center, counter-clockwise from the sketch's +x axis.
ring(count, step=None, start=0.0)count angles in degrees, starting at start and step apart. The default step, 360 / count, makes a closed ring whose last item stops one step short of the first.
grid(nx, ny, dx, dy, origin=(0.0, 0.0))nx × ny (x, y) positions, dx and dy apart, row by row with x varying fastest; the seed position comes first.

A ring of points that stays linked to its source — what Modify → Pattern… writes:

step = cad.variable(60, driving=True)          # editable in the Variables panel
c = cad.point2d(0, 0)
p0 = cad.point2d(25, 0)
l0 = cad.line2d(c, p0, construction=True)      # every copy is tied to this source
for k, angle in enumerate(ring(6)):
    if k == 0:
        continue
    pk = cad.point2d(*polar(25, angle))
    lk = cad.line2d(c, pk, construction=True)
    cad.equal_lines(line1=l0, line2=lk)
    cad.angle(line1=l0, line2=lk, angle=step * k)

Component libraries

Catalog parts come from component libraries — folders of parametric part modules. A standard library ships in the app; add your own as folders under ~/Documents/FlywheelCAD/Libraries/. A library factory takes cad first and returns a component name, so it drops straight into the assembly flow — call it, then cad.instance(...) the result and mate to its exported anchors.

from lib.standard.steppers import stepper
from lib.standard.bearings import bearing

part = stepper(cad, size=17, length=40)                # a NEMA 17, 40 mm long
motor = cad.instance(part)
brg = cad.instance(bearing(cad, designation="625"))    # 5 mm bore, to fit the shaft
cad.mate_coincident(brg, "face_b", motor.shaft_tip)

Inserting through Component → Insert from Library… is copy-on-use: the module is vendored into the document’s lib/<library>/ folder (with a provenance header, shown as a read-only tab), so the design stays self-contained and a later library update never silently changes it. A few of the standard modules (every factory also accepts quality=):

lib.standard.steppersstepper(cad, size=17, length=None, shaft_length=None, body="square") — NEMA 8, 11, 14, 17, 23 and 34 stepper motors, square or round body. Anchors mount_face, shaft_tip, shaft_base, bolt_0–bolt_3.
lib.standard.servosservo(cad, size="standard"), horn(cad, style="single", spline="standard", length=None) — hobby servos and control horns.
lib.standard.fastenerscap_screw(cad, thread="M3", length=12.0, threads="cosmetic"), hex_nut(cad, thread="M3", threads="cosmetic"), washer(cad, thread="M3"), clearance(cad, thread="M3", length=12.0, head_pocket=False), tap(cad, thread="M3", length=10.0) — metric hardware and the matching hole bodies to subtract.
lib.standard.bearingsbearing(cad, designation="608", flanged=False) — deep-groove ball bearings. Anchors face_a, face_b, center.
lib.standard.extrusionsrail(cad, profile="2020", length=100.0) — T-slot aluminium extrusion (20, 30 and 40 series).
lib.standard.linkageball_link(cad, thread="M2"), clevis(cad, thread="M2"), pushrod(cad, diameter=2.0, length=80.0) — RC linkage hardware.

The full catalogue — gears, electronics, framing, mounts and more — is on the Component library page.

Standalone parts: to place a loose component .py that isn’t in a library, use Component → Insert Component from File… — it copies the file into the project and appends the import + instance lines. The MotorMount project in the samples download is a full standard-library showcase.

Including a separate Python file

Put the helper module next to the main script — inside the bundle, next to main.py, for a .fwcad document. The document folder is on the import path, so from shapes import rectangle finds shapes.py, as in the Quick start. A helper calls FlywheelCAD() itself and gets the same shared object as the main script, so names stay unique across files.

MyProject.fwcad/
  main.py
  shapes.py
  profiles.py

Imported modules appear as read-only tabs in the script pane. Don't add __init__.py files, and don't give a module the name of a Python standard-library module or one of flywheelcad, sitecustomize, component_shape, component_export_rewrite, document_setting_rewrite or test.

Flat file or project bundle: a document is either a plain .py (helper modules and any vendored lib/ sit next to it) or a .fwcad project bundle — a folder whose main.py is the script, packaging every imported file so it travels as one self-contained item. The import rules are identical either way. To pull an existing helper into the project, use File → Add File to Project… (it copies the file in under an importable module name); File → Convert to Project Bundle… and Export as Flat Script… convert between the two formats.