Repeat
repeat() re-applies a feature at several positions. The feature — a cut, an extrude, a fillet, a rib — runs again at each new location and interacts with the solid there, exactly as if you had modelled it there by hand. A repeated cut gives one solid with many pockets; a copy of the same cut would give many solids.
// copy() clones the whole shape: separate solids with one pocket each
cut(15)
copy("linear", "x", { count: 4, offset: 40 })
// repeat() re-applies the cut: one solid with 4 pockets
const c = cut(15)
repeat("linear", "x", { count: 4, offset: 40 }, c)
In the viewport
- Click Repeat on the toolbar. The Repeat dialog docks on the right.
- Pick the kind: Linear, Circular, Mirror or Rotate.
- Click the features to repeat — their rows in the timeline, or their geometry in the viewport. They fill the Features slot as chips; several features repeat together.
- Linear: fill the Direction 1 axis (click a world axis shown in the viewport, an
axis()feature, or an edge), the Total Count and the spacing (Offset between neighbours, or Total span). Add second direction makes a grid. Circular and Rotate take one Axis (empty defaults to world Z) and an Angle; Mirror swaps the axis for a Plane slot (an origin-plane quad, a plane feature, or a face). - Centered and Skip as in the Copy dialog.
- Click Apply. The timeline gains a Repeat row after the repeated features.
Linear repeat

The code behind it
The last argument is the feature to repeat — the value cut() returned. Two axes and two counts make a 7 × 2 grid; length spreads the instances evenly over the given span.
import { sketch, extrude, cut, repeat, line } from 'fluidcad/core';
import { coincident, distance, fix, horizontal, vertical } from "fluidcad/constraints";
sketch("xy", () => {
const sg1 = line([-150, -52], [150, -52]);
const sg2 = line([150, -52], [150, 52]);
const sg3 = line([150, 52], [-150, 52]);
const sg4 = line([-150, 52], [-150, -52]);
coincident(sg1.end(), sg2.start());
coincident(sg2.end(), sg3.start());
coincident(sg3.end(), sg4.start());
coincident(sg4.end(), sg1.start());
horizontal(sg1);
vertical(sg2);
horizontal(sg3);
vertical(sg4);
fix(sg1.start(), [-150, -52]);
distance(sg1.start(), sg1.end(), 300);
distance(sg2.start(), sg2.end(), 104);
})
// The tray body.
const tray = extrude(50)
sketch(tray.endFaces(), () => {
const sg5 = line([-143, -45], [-113, -45]);
const sg6 = line([-113, -45], [-113, -5]);
const sg7 = line([-113, -5], [-143, -5]);
const sg8 = line([-143, -5], [-143, -45]);
coincident(sg5.end(), sg6.start());
coincident(sg6.end(), sg7.start());
coincident(sg7.end(), sg8.start());
coincident(sg8.end(), sg5.start());
horizontal(sg5);
vertical(sg6);
horizontal(sg7);
vertical(sg8);
fix(sg5.start(), [-143, -45]);
distance(sg5.start(), sg5.end(), 30);
distance(sg6.start(), sg6.end(), 40);
})
// One pocket, cut 15 deep into the top face.
const c = cut(15)
// Re-apply the cut on a 7 × 2 grid: 7 across 255 along X, 2 across 50 along Y.
// `length` is the whole span, so the pockets spread evenly over it.
repeat("linear", ["x", "y"], {
count: [7, 2],
length: [255, 50]
}, c)
Options:
| Option | Meaning |
|---|---|
count | repetitions per axis, the original included |
length | total span per axis, instances spread evenly |
offset | explicit spacing per axis, instead of length |
centered | centre the pattern on the original — count: 3 puts one instance each side |
skip | index tuples to leave out: [[1], [3]] on one axis, [[1, 2]] for a grid cell |
Use length when the overall size is known, offset when the pitch is.
Circular repeat

The code behind it
import { sketch, circle, extrude, cut, repeat } from 'fluidcad/core';
// A pipe flange: a disc with a central bore and a bolt circle of six holes.
sketch("xy", () => {
circle([0, 0], 120); // flange outer diameter
circle([0, 0], 40); // the bore, a hole in the profile
});
const flange = extrude(12);
// One bolt hole on the bolt circle, radius 45.
sketch(flange.endFaces(), () => { circle([45, 0], 10); });
const bolt = cut();
// Repeat the CUT six times around Z: one solid with six holes. copy() would
// instead clone the whole flange.
repeat("circular", "z", { count: 6, angle: 360 }, bolt);
Options: count, angle (the whole sweep), offset (degrees between neighbours, instead of angle), centered, skip (a flat list of indices, [2, 4]).
Mirror repeat
Re-applies the feature reflected across a plane — for symmetric parts you only model one half of:

The code behind it
Only the features passed are mirrored. The chamfer is left out of the call, so it exists on one side only.
import { chamfer, cut, extrude, fillet, plane, repeat, select, sketch } from 'fluidcad/core';
import { edge, } from 'fluidcad/filters';
import { circle, line } from 'fluidcad/core';
import { coincident, distance, fix, horizontal, vertical } from "fluidcad/constraints";
sketch(plane("xy"), () => {
const sg1 = line([-100, -50], [100, -50]);
const sg2 = line([100, -50], [100, 50]);
const sg3 = line([100, 50], [-100, 50]);
const sg4 = line([-100, 50], [-100, -50]);
coincident(sg1.end(), sg2.start());
coincident(sg2.end(), sg3.start());
coincident(sg3.end(), sg4.start());
coincident(sg4.end(), sg1.start());
horizontal(sg1);
vertical(sg2);
horizontal(sg3);
vertical(sg4);
fix(sg1.start(), [-100, -50]);
distance(sg1.start(), sg1.end(), 200);
distance(sg2.start(), sg2.end(), 100);
})
const e1 = extrude(20)
sketch(e1.endFaces(), () => {
const sg5 = line([80, 30], [100, 30]);
const sg6 = line([100, 30], [100, 50]);
const sg7 = line([100, 50], [80, 50]);
const sg8 = line([80, 50], [80, 30]);
coincident(sg5.end(), sg6.start());
coincident(sg6.end(), sg7.start());
coincident(sg7.end(), sg8.start());
coincident(sg8.end(), sg5.start());
horizontal(sg5);
vertical(sg6);
horizontal(sg7);
vertical(sg8);
fix(sg5.start(), [80, 30]);
distance(sg5.start(), sg5.end(), 20);
distance(sg6.start(), sg6.end(), 20);
})
cut()
sketch(e1.endFaces(), () => {
const sg9 = line([-100, -50], [-15, -50]);
const sg10 = line([-15, -50], [-15, -30]);
const sg11 = line([-15, -30], [-100, -30]);
const sg12 = line([-100, -30], [-100, -50]);
coincident(sg9.end(), sg10.start());
coincident(sg10.end(), sg11.start());
coincident(sg11.end(), sg12.start());
coincident(sg12.end(), sg9.start());
horizontal(sg9);
vertical(sg10);
horizontal(sg11);
vertical(sg12);
fix(sg9.start(), [-100, -50]);
distance(sg9.start(), sg9.end(), 85);
distance(sg10.start(), sg10.end(), 20);
})
const e2 = extrude(50);
sketch(e2.sideFaces(3), () => {
circle([-58, 42.5], 30);
})
const c1 = cut(20)
select(edge().onPlane("top", 20+50).parallelTo("yz"))
chamfer(15)
select(edge().onPlane("top", 20).onPlane("yz", -15))
const f2 = fillet(10)
// Re-apply the wall extrude, the hole cut and the fillet on the far side of the
// front plane. The chamfer is not passed, so it stays one-sided.
repeat("mirror", "front", e2, c1, f2);
const e = extrude(20)
repeat("mirror", "front", e) // one feature
repeat("mirror", "front", e, c1, f2) // several at once
Rotate repeat
A single rotated clone of a feature around an axis:
const e = extrude(10)
repeat("rotate", "z", 45, e) // 45° around Z
repeat("rotate", "z", e) // the angle defaults to 90°
Matrix repeat
Any other transform is passed as a Matrix4:
import { Matrix4 } from 'fluidcad/math';
const m = Matrix4.fromTranslation(10, 0, 5).multiply(
Matrix4.fromRotationZ(Math.PI / 6)
)
repeat(m, e) // translation + rotation in one step
Example: ice cube tray
Cut one pocket, then repeat it across the tray:
The code behind it
import { sketch, extrude, cut, repeat, line } from 'fluidcad/core';
import { coincident, distance, fix, horizontal, vertical } from "fluidcad/constraints";
sketch("xy", () => {
const sg1 = line([-150, -52], [150, -52]);
const sg2 = line([150, -52], [150, 52]);
const sg3 = line([150, 52], [-150, 52]);
const sg4 = line([-150, 52], [-150, -52]);
coincident(sg1.end(), sg2.start());
coincident(sg2.end(), sg3.start());
coincident(sg3.end(), sg4.start());
coincident(sg4.end(), sg1.start());
horizontal(sg1);
vertical(sg2);
horizontal(sg3);
vertical(sg4);
fix(sg1.start(), [-150, -52]);
distance(sg1.start(), sg1.end(), 300);
distance(sg2.start(), sg2.end(), 104);
})
const tray = extrude(50)
// One pocket
sketch(tray.endFaces(), () => {
const sg5 = line([-143, -45], [-113, -45]);
const sg6 = line([-113, -45], [-113, -5]);
const sg7 = line([-113, -5], [-143, -5]);
const sg8 = line([-143, -5], [-143, -45]);
coincident(sg5.end(), sg6.start());
coincident(sg6.end(), sg7.start());
coincident(sg7.end(), sg8.start());
coincident(sg8.end(), sg5.start());
horizontal(sg5);
vertical(sg6);
horizontal(sg7);
vertical(sg8);
fix(sg5.start(), [-143, -45]);
distance(sg5.start(), sg5.end(), 30);
distance(sg6.start(), sg6.end(), 40);
})
const pocket = cut(30).draft(-10)
// Repeat the pocket in a 7x2 grid
repeat("linear", ["x", "y"], {
count: [7, 2],
length: [255, 50]
}, pocket)
Each repetition cuts into the same tray — one solid with 14 pockets.

Selecting one instance
repeat() returns the pattern, and instance(k) addresses one of its instances without describing where it is. When the repeat clones a single feature, the instance forwards that feature's selection accessors, so a clone is selected exactly like the original:
const e = extrude(10)
const r = repeat("linear", "x", { count: 3, offset: 40 }, e)
fillet(2, e.endEdges()) // the original's top rim
fillet(2, r.instance(1).endEdges()) // the middle instance's top rim
fillet(2, r.instance(2).sideEdges(0)) // one side edge of the last instance
The instance is also a whole geometry: fillet(2, r.instance(1)) rounds every edge of that instance, and select(edge().from(r.instance(1)).circle()) scopes a filter to it — the form to use when the repeat clones several features at once, where a forwarded accessor would be ambiguous.
Slots are numbered like the copy tool's: circular, mirror, rotate and matrix repeats keep the original at slot 0; a linear repeat linearizes its grid in axis order (the first axis varies slowest) with the original at its own cell — slot 0, or the centre cell when centered. The numbering is the one skip uses, and a skipped slot is an error to address.
Picking a clone's edges in the viewport writes this form: the Fillet tool on the middle instance's rim emits r.instance(1).endEdges() and binds the repeat statement to r. When the repeat clones several features, or the pick lands on geometry a repeated fillet or chamfer took over, the tool scopes a select() to the instance instead — select(edge().onPlane('xy').from(r.instance(1))). Picking the same edges on every instance emits one scene-wide select() for the pattern.
Ribs and other features
Anything a create feature returns can be repeated, including a rib fanned around an axis, a fillet, or a set of features together. Repeating a feature that was applied to a selection re-maps the selection to the cloned geometry automatically.