Patterns
Real-world objects often have repeated parts. Consider a LEGO brick, which has a lot of repeated bumps on its top face. Or a table, with four repeated legs. KCL would be a very tedious language if we made you define each leg, or each LEGO bump, over and over again every time your model needed one. Luckily, there's a simple way to repeat geometry in your model. It's called a pattern. There are several ways to use patterns. Let's learn how they work!
Basic patterns
Let's start simple. We can use patterns to replicate our geometry, copying it into our scene several times. Let's take this simple cylinder, and copy it 4 times.
cylinders = startSketchOn(XY)
|> circle(radius = 4, center = [0, 0])
|> extrude(length = 10)
|> patternLinear3d(instances = 4, distance = 10, axis = [1, 0, 0])
patternLinear3d
function takes 4 args:
- A solid to pattern (the unlabeled first arg, which is implicitly set to % and therefore gets the cylinder piped in)
- The total number of instances you want (i.e. how many total copies of the solid there should be)
- How far apart each instance of the pattern should be
- The axis along which to place the copies.
In our above example, [1, 0, 0]
is the X axis, so it places 4 instance along the X axis, each 10 units apart.
Circular patterns
You can also use patterns to replicate something and lay them out in an arc around a point. We'll use the patternCircular3d
function. Here's an example where we put 12 cubes in a circle:
offset = 40
cubes = startSketchOn(XZ)
|> polygon(numSides = 4, radius = 10, center = [0, offset])
|> extrude(length = 10)
|> patternCircular3d(
instances = 12,
axis = [0, 1, 0],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = false,
)
[0, 0, 0]
. We drew the first cube at the northernmost position (12 o'clock) and all the other instances were patterned around that center. Nice!
Notice that we used rotateDuplicates = false
. As the name implies, this argument controls whether the duplicates get rotated, so that they're always facing the same way with regards to the center. If we set it to true, we get this:
offset = 40
cubes = startSketchOn(XZ)
|> polygon(numSides = 4, radius = 10, center = [0, offset])
|> extrude(length = 10)
|> patternCircular3d(
instances = 12,
axis = [0, 1, 0],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
arcDegrees
argument, we could pattern around only part of the circle instead. Let's do two thirds of the circle:
offset = 40
cylinders = startSketchOn(XZ)
|> polygon(numSides = 4, radius = 10, center = [0, offset])
|> extrude(length = 10)
|> patternCircular3d(
instances = 12,
axis = [0, 1, 0],
center = [0, 0, 0],
arcDegrees = 240,
rotateDuplicates = true,
)
base = startSketchOn(XZ)
|> circle(radius = 50, center = [0, 0])
|> extrude(length = 10)
offset = 30
boxes = startSketchOn(base, face = END)
|> circle(radius = 5, center = [0, offset])
|> extrude(length = 10)
|> patternCircular3d(
instances = 6,
axis = [0, 1, 0],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
Transform patterns
Circular and linear patterns cover a lot of really common use-cases for mechanical engineers. But sometimes you want to do more complicated patterns, in more complicated shapes. We can't add a dedicated pattern function for every single shape our users can think of -- that would be ridiculous. Instead, we've got a powerful, flexible interface for patterning solids in any arrangement you can think of. It's called a transform pattern. They're created with the patternTransform
function. It takes a familiar instances
arg, which controls how many total copies of the shape you want. But it takes a new argument, called transform
. This is a custom function. We'll dive deeper into those in the following chapters, but for now, they're basically just a way to calculate how to transform each replica in the pattern.
When might you need a pattern transform? Here's one use: to do a 2D pattern, like tiling a grid. Let's use a pattern transform to make a 5 by 5 grid.
n = 5
width = 10
gap = 1.5 * width
// Transform function
fn grid(@i) {
column = rem(i, divisor = n)
row = floor(i / n)
return { translate = [column * gap, row * gap, 0] }
}
startSketchOn(XY)
|> polygon(numSides = 4, radius = width, center = [0, 0])
|> extrude(length = 2)
|> rotate(yaw = 45)
|> patternTransform(instances = n * n, transform = grid)
We've defined a custom function called grid
. This function will get called once for every replica in the pattern, and it tells KCL how each replica should be transformed. Specifically it:
- Takes a single argument called
i
. It's used to indicate which number replica it is. The first copy made will seti
to 1, the second copy will seti
to 2, etc etc. The argumenti
is prefixed with@
to indicate it's this function's special first unlabeled arg, so if you call it, you'd call it likegrid(1)
orgrid(2)
, notgrid(i = 1)
. - Returns a list of different properties to transform in each replica.
In this example, we declare a function grid
which tells patternTransform
to translate each replica by a certain amount column * gap
along X axis, row * gap
along the Y axis, and to stay on the same Z axis (i.e. move exactly 0 along that axis).
The specific value of row
and column
changes every time the grid
function is called, because these variables are calculated from the input argument i
. Remember, i
represents which number replication we're transforming. To calculate column
and row
we're going to use a few new KCL functions we haven't seen before.
Firstly, rem
. The value rem(i, divisor = n)
will divide i by n and return the remainder. This means that for i = 0, 1, 2, 3, 4, x will equal 0, 1, 2, 3 and 4. But when i = 5 (i.e. the fifth copy is being calculated), x will be 0. We're calling this function 25 times, and over those calls, x will step from 0 to 4, jump back down to 0, and begin stepping up again. This means x is a good way to calculate the columns, which range from column 0 to column 4 (a total of 5 columns).
The floor
function takes a fractional number, and rounds it down to the nearest integer. For example, floor(3.6)
is 3. This means it's a good way to calculate the row, because the first five times it's called, row
will always equal 0. It'll round down (i / n)
from 0/5, 1/5, 2/5, 3/5, 4/5 all down to 0. Then the sixth time it's called, it will receive 5/5, which is 1, and round it down to 1. These neat little mathematical tricks mean we can calculate the row and column from the repetition number i
.
The final result speaks for itself:
n = 5
width = 10
gap = 1.5 * width
// Transform function
fn chessboard(@i) {
row = rem(i, divisor = n)
column = floor(i / n)
isEven = rem(i, divisor = 2) == 0
return [{ translate = [row * gap, column * gap, 0], replicate = isEven }]
}
startSketchOn(XY)
|> polygon(numSides = 4, radius = width, center = [0, 0])
|> extrude(length = 2)
|> rotate(yaw = 45)
|> patternTransform(instances = n * n, transform = chessboard)
In this example, we use a very similar transform function. The only difference is, we're setting the replicate
property on the final transform too. And we're setting it to the variable isEven
. This variable is a boolean value -- it's true if i
divided by 2 has a remainder of 0, which is the definition of an even number (it's divisible by 2). This should skip every second replication. Let's try it out!
width = 20
fn transform(@i) {
return {
// Move down each time.
translate = [0, 0, -i * width],
// Make the cube longer, wider and flatter each time.
scale = [
pow(1.1, exp = i),
pow(1.1, exp = i),
pow(0.9, exp = i)
],
// Turn by 15 degrees each time.
rotation = { angle = 15 * i, origin = "local" }
}
}
cube = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> polygon(numSides = 4, radius = width, center = [width, width])
|> extrude(length = width)
cube |> patternTransform(instances = 25, transform = transform)
In this example, we make 25 cubes, slightly transforming each one. Each cube gets translated (moving down along the Z axis), and scaled (becoming longer, wider and flatter), as well as rotating 15 degrees around its own center (i.e. its local origin). We could rotate them around the scene's center by using origin = "global"
. Here's the result.
Pattern transforms are a very powerful tool. They're definitely one of the most complex function in KCL, but that complexity gives you a lot of flexibility. Any mathematical curve you can formulate can be used to pattern your instances, by just calculating it in a transform function. The same goes for tiling or grid arrangements. For more examples, you can read the full patternTransform
docs.
2D patterns and holes
So far all of the patterns we've used have replicated 3D solids. But you can use patterns to replicate 2D sketches too. The patternLinear2d
, patternCircular2d
and patternTransform2d
functions work like their 3D variants, except they take 2D axes and 2D points. Here's a simple example:
manyCircles = startSketchOn(XZ)
|> circle(radius = 4, center = [50, 0])
|> patternCircular2d(
center = [0, 0],
instances = 12,
arcDegrees = 360,
rotateDuplicates = true,
)
extrude(manyCircles, length = 10)
to the end of the above KCL program. But it's not a good idea, because it produces the exact same model as you would have gotten from making a single 3D solid, then using 3D patterns on that. The only difference is, extruding a 2D pattern is much slower than patterning a 3D solid. So, can we do anything useful with 2D patterns?
Yes! One important use case is putting holes into 2D sketches. We have a special subtract2d
function for this. Let's take the pattern from above, and use it to cut holes into another sketch.
manyCircles = startSketchOn(XZ)
|> circle(radius = 4, center = [50, 0])
|> patternCircular2d(
center = [0, 0],
instances = 12,
arcDegrees = 360,
rotateDuplicates = true,
)
base = startSketchOn(XZ)
|> circle(radius = 60, center = [0, 0])
|> subtract2d(tool = manyCircles)
|> extrude(length = 10)
subtract2d
, you should consider it.