Right now, if you model something like this box with a button: <img width="413" alt="Screenshot 2025-02-06 at 3 08 03 PM" src="https://github.com/user-attachments/assets/04818a70-7cf3-4ee3-b8c5-df5959ac10db" /> Let's say you want to pattern the button, and repeat it a second time. If you try, you'll actually pattern the entire model (box + button). <img width="486" alt="Screenshot 2025-02-06 at 3 08 52 PM" src="https://github.com/user-attachments/assets/09fc28d9-5d80-4ab3-b4dc-b8de2945fcba" /> Why? Because right now, when you sketch on a face (like the button was), both the box and the button share the same ID. All extrusions from a solid will share the same ID, because they all refer to the same composite solid. This is helpful in some ways -- arguably the solid _is_ just one big complex shape now -- but it's not helpful in other ways. What if I want to only pattern the button? Luckily there's an original ID for the button part, which is still stored. So we just need a way to tell the pattern stdlib functions whether to use the target's main ID or its original ID. This PR adds a new optional bool, `useOriginal`, to patterns. It's false by default, to keep backwards-compatibility (make sure that old KCL code doesn't change). This PR is based on https://github.com/KittyCAD/modeling-app/pull/3914. It's based on work Serena and I are doing to fix a bug (engine does not allow patterning a 3D solid which was sketched on a face of another solid). @gserena01 our test program is now: ``` w = 400 case = startSketchOn('XY') |> startProfileAt([-w, -w], %) |> line(endAbsolute = [-w, w]) |> line(endAbsolute = [w, -w]) |> line(endAbsolute = [-w, -w]) |> close() |> extrude(length = 200) bump1 = startSketchOn(case, 'end') |> circle({ center = [-50, -50], radius = 40 }, %) |> extrude(length = 20) // We pass in "bump1" here since we want to pattern just this object on the face. useOriginal = true target = bump1 transform = { axis = [1, 0, 0], instances = 3, distance = -100 } patternLinear3d(transform, target, useOriginal) ``` If you change the `useOriginal = true` to `false` you can see the difference.
747 KiB
title, excerpt, layout
title | excerpt | layout |
---|---|---|
patternTransform | Repeat a 3-dimensional solid, changing it each time. | manual |
Repeat a 3-dimensional solid, changing it each time.
Replicates the 3D solid, applying a transformation function to each replica. Transformation function could alter rotation, scale, visibility, position, etc.
The patternTransform
call itself takes a number for how many total instances of the shape should be. For example, if you use a circle with patternTransform(4, transform)
then there will be 4 circles: the original, and 3 created by replicating the original and calling the transform function on each.
The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument 1
. This simplifies your math: the transform function can rely on id 0
being the original instance passed into the patternTransform
. See the examples.
The transform function returns a transform object. All properties of the object are optional, they each default to "no change". So the overall transform object defaults to "no change" too. Its properties are:
-
translate
(3D point)Translates the replica, moving its position in space.
-
replicate
(bool)If false, this ID will not actually copy the object. It'll be skipped.
-
scale
(3D point)Stretches the object, multiplying its width in the given dimension by the point's component in that direction.
-
rotation
(object, with the following properties)-
rotation.axis
(a 3D point, defaults to the Z axis) -
rotation.angle
(number of degrees) -
rotation.origin
(either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
-
patternTransform(total_instances: integer, transform_function: FunctionParam, solid_set: SolidSet, use_original?: bool) -> [Solid]
Arguments
Name | Type | Description | Required |
---|---|---|---|
total_instances |
integer |
Yes | |
transform_function |
FunctionParam |
Yes | |
solid_set |
SolidSet |
A solid or a group of solids. | Yes |
use_original |
bool |
No |
Returns
Examples
// Each instance will be shifted along the X axis.
fn transform(id) {
return { translate = [4 * id, 0, 0] }
}
// Sketch 4 cylinders.
sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(length = 5)
|> patternTransform(4, transform, %)
// Each instance will be shifted along the X axis,
// with a gap between the original (at x = 0) and the first replica
// (at x = 8). This is because `id` starts at 1.
fn transform(id) {
return { translate = [4 * (1 + id), 0, 0] }
}
sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(length = 5)
|> patternTransform(4, transform, %)
fn cube(length, center) {
l = length / 2
x = center[0]
y = center[1]
p0 = [-l + x, -l + y]
p1 = [-l + x, l + y]
p2 = [l + x, l + y]
p3 = [l + x, -l + y]
return startSketchOn('XY')
|> startProfileAt(p0, %)
|> line(endAbsolute = p1)
|> line(endAbsolute = p2)
|> line(endAbsolute = p3)
|> line(endAbsolute = p0)
|> close()
|> extrude(length = length)
}
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, i), pow(1.1, i), pow(0.9, i)],
// Turn by 15 degrees each time.
rotation = { angle = 15 * i, origin = "local" }
}
}
myCubes = cube(width, [100, 0])
|> patternTransform(25, transform, %)
fn cube(length, center) {
l = length / 2
x = center[0]
y = center[1]
p0 = [-l + x, -l + y]
p1 = [-l + x, l + y]
p2 = [l + x, l + y]
p3 = [l + x, -l + y]
return startSketchOn('XY')
|> startProfileAt(p0, %)
|> line(endAbsolute = p1)
|> line(endAbsolute = p2)
|> line(endAbsolute = p3)
|> line(endAbsolute = p0)
|> close()
|> extrude(length = length)
}
width = 20
fn transform(i) {
return {
translate = [0, 0, -i * width],
rotation = {
angle = 90 * i,
// Rotate around the overall scene's origin.
origin = "global"
}
}
}
myCubes = cube(width, [100, 100])
|> patternTransform(4, transform, %)
// Parameters
r = 50 // base radius
h = 10 // layer height
t = 0.005 // taper factor [0-1)
// Defines how to modify each layer of the vase.
// Each replica is shifted up the Z axis, and has a smoothly-varying radius
fn transform(replicaId) {
scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8))
return {
translate = [0, 0, replicaId * 10],
scale = [scale, scale, 0]
}
}
// Each layer is just a pretty thin cylinder.
fn layer() {
return startSketchOn("XY")
// or some other plane idk
|> circle({ center = [0, 0], radius = 1 }, %, $tag1)
|> extrude(length = h)
}
// The vase is 100 layers tall.
// The 100 layers are replica of each other, with a slight transformation applied to each.
vase = layer()
|> patternTransform(100, transform, %)
fn transform(i) {
// Transform functions can return multiple transforms. They'll be applied in order.
return [
{ translate = [30 * i, 0, 0] },
{ rotation = { angle = 45 * i } }
]
}
startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> polygon({
radius = 10,
numSides = 4,
center = [0, 0],
inscribed = false
}, %)
|> extrude(length = 4)
|> patternTransform(3, transform, %)