Skip to main content

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

  1. Click Repeat on the toolbar. The Repeat dialog docks on the right.
  2. Pick the kind: Linear, Circular, Mirror or Rotate.
  3. 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.
  4. 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).
  5. Centered and Skip as in the Copy dialog.
  6. Click Apply. The timeline gains a Repeat row after the repeated features.

Linear repeat

A grid of cuts

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.

tray.part.js
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:

OptionMeaning
countrepetitions per axis, the original included
lengthtotal span per axis, instances spread evenly
offsetexplicit spacing per axis, instead of length
centeredcentre the pattern on the original — count: 3 puts one instance each side
skipindex 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

A pipe flange with a bolt circle of six holes

The code behind it
flange.part.js
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:

Extrude, cut and fillet mirrored across the front plane; the chamfer is not

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.

bracket.part.js
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
ice-tray.part.js
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.

Ice cube tray with repeated 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.