Compare commits
11 Commits
codex/fix-
...
achalmers/
Author | SHA1 | Date | |
---|---|---|---|
5bc6e76d7b | |||
556396acf3 | |||
56c25cd1f7 | |||
3cd9454268 | |||
55d27ee391 | |||
f35485751d | |||
2b7e924eb9 | |||
63f2756182 | |||
1a1c2c038b | |||
7e2a75509b | |||
4b7537356f |
@ -1,4 +0,0 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/**/*.md,./e2e/playwright/lib/console-error-whitelist.ts,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts,./test-results,./playwright-report,./kcl-book/book
|
||||
|
5
.gitignore
vendored
@ -86,3 +86,8 @@ venv
|
||||
.vscode-test
|
||||
.biome/
|
||||
.million
|
||||
|
||||
# KCL book
|
||||
kcl-book/book
|
||||
kcl-book/src/images/dynamic/foo.png
|
||||
|
||||
|
1
kcl-book/TODO.md
Normal file
@ -0,0 +1 @@
|
||||
|
15
kcl-book/book.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[book]
|
||||
authors = ["Adam Chalmers"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Modeling with KCL"
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
renderer = ["html"]
|
||||
|
||||
# [output.pdf]
|
||||
|
||||
# [output.html.print]
|
||||
# enable = true
|
BIN
kcl-book/program1_in_editor.png
Normal file
After Width: | Height: | Size: 738 KiB |
31
kcl-book/src/SUMMARY.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Contents
|
||||
|
||||
[Introduction](./intro.md)
|
||||
|
||||
# Language basics
|
||||
|
||||
- [Calculations and variables](./variables.md)
|
||||
- [Calling functions](./calling_functions.md)
|
||||
- [Pipeline syntax](./pipelines.md)
|
||||
|
||||
# Modeling with KCL
|
||||
|
||||
- [Sketching 2D shapes](./sketch2d.md)
|
||||
- [Sketching curved lines](./sketch2d_curves.md)
|
||||
- [Creating 3D shapes](./sketch3d.md)
|
||||
- [Tags, Fillets and Chamfers](./tags.md)
|
||||
- [Sketch on face](./sketch_on_face.md)
|
||||
- [Transforming 3D solids](./transform_3d.md)
|
||||
- [Combining 3D solids](./csg.md)
|
||||
- [Patterns](./patterns.md)
|
||||
|
||||
# Advanced modeling
|
||||
|
||||
- [Functions and parametric design](./declaring_functions.md)
|
||||
- [Iterating with map and reduce](./map_reduce.md)
|
||||
- [Reduce and geometry](./reduce_geometry.md)
|
||||
|
||||
# KCL projects
|
||||
- [Units of measurement](./units_of_measurement.md)
|
||||
- [Modules](./modules.md)
|
||||
|
110
kcl-book/src/calling_functions.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Calling functions
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
In the last chapter, we looked at different data types that KCL can store. Now let's look at how to actually use them for more complex calculations. We use KCL functions for nearly everything, including all our mechanical engineering tools, so they're very important.
|
||||
|
||||
## Data in, data out
|
||||
|
||||
Let's look at a really simple function call.
|
||||
|
||||
```kcl
|
||||
smallest = min([1, 2, 3, 0, 4])
|
||||
```
|
||||
|
||||
This is a variable declaration, just like the variables we declared in the previous chapter. But the right-hand side -- the value the variable is defined as -- looks different. This is a _function call_. The function's name is `min`, as in "minimum".
|
||||
|
||||
Functions have _inputs_ and _outputs_. This function has just one input, an array of numbers. When you _call_ a function, you pass it inputs in between the parentheses/round brackets. Then KCL calculates its output. You can check its output by looking up `smallest` in the Variables panel. Spoiler: it's 0. Which is, as you'd expect, the minimum value in that array.
|
||||
|
||||
If you hover your mouse cursor over the function name `min`, you'll find some helpful documentation about the function. You can also look up all the possible functions at <https://docs.zoo.dev>. That page shows every function, and if you click it, you can see the function's name, inputs, outputs and some helpful examples of how to use it.
|
||||
|
||||
All functions take some data inputs and return an output. The inputs can be variables, just like you used in the previous chapter:
|
||||
|
||||
```kcl
|
||||
myNumbers = [1, 2, 3, 0, 4]
|
||||
smallest = min(myNumbers)
|
||||
```
|
||||
|
||||
A function's inputs are also called its _arguments_. A function's output is also called its _return value_.
|
||||
|
||||
Here are some other simple functions you can call:
|
||||
|
||||
```kcl
|
||||
absoluteValue = abs(-3)
|
||||
roundedUp = ceil(12.5)
|
||||
shouldBe2 = log10(100)
|
||||
```
|
||||
|
||||
## Labeled arguments
|
||||
|
||||
The `min` function takes just one argument: an array of numbers. But most KCL functions take in multiple arguments. When there's many different arguments, it can be confusing to tell which argument means what. For example, what does this function do?
|
||||
|
||||
```kcl
|
||||
x = pow(4, 2)
|
||||
```
|
||||
|
||||
If you mouse over the docs for `pow` (or look them up at the KCL website) you'll see it's short for `power`, as in raising a number to some power (like squaring it, or cubing it). But, does `pow(4, 2)` mean 4 to the power of 2, or 2 to the power of 4? You could look up the docs, but that gets annoying quickly. Instead, KCL uses _labels_ for the parameters. The real `pow` call looks like this:
|
||||
|
||||
```kcl
|
||||
x = pow(4, exp = 2)
|
||||
```
|
||||
|
||||
Now you can tell that 2 is the _exponent_ (i.e. the power), not the base. If a KCL function has multiple arguments, only the first argument can be unlabeled. All the following arguments need a label. Here are some other examples.
|
||||
|
||||
```kcl
|
||||
oldArray = [1, 2, 3]
|
||||
newArray = push(oldArray, item = 4)
|
||||
```
|
||||
|
||||
Here, we make a new array by pushing a new item onto the end of the old array. The old array is the first argument, so it doesn't need a label. The second argument, `item`, does need a label.
|
||||
|
||||
## Combining functions
|
||||
|
||||
Functions take inputs and produce an output. The real power of functions is: that output can become the input to another function! For example:
|
||||
|
||||
```kcl
|
||||
x = 2
|
||||
xSquared = pow(x, exp = 2)
|
||||
xPow4 = pow(xSquared, exp = 2)
|
||||
```
|
||||
|
||||
That's a very simple example, but it shows that you can assign the output of a function call to a variable (like `xSquared`) and then use it as the input to another function. Here's a more realistic example, where we use several functions to calculate the roots x0 and x1 of a quadratic equation.
|
||||
|
||||
```kcl
|
||||
a = 2
|
||||
b = 3
|
||||
c = 1
|
||||
|
||||
delta = pow(b, exp = 2) - (4 * a * c)
|
||||
x0 = ((-b) + sqrt(delta)) / (2 * a)
|
||||
x1 = ((-b) - sqrt(delta)) / (2 * a)
|
||||
```
|
||||
|
||||
If you open up the Variables panel, you'll see this gives two roots -0.5 and -1. Combining functions like this lets you break complicated equations into several small, simple steps. Each step can have its own variable, with a sensible name that explains how it's being used.
|
||||
|
||||
## Comments
|
||||
|
||||
This is a good point to introduce comments. When you start writing more complex code, with lots of function calls and variables, it might be hard for your colleagues (or your future self) to understand what you're trying to do. That's why KCL lets you leave comments to anyone reading your code. Let's add some comments to the quadratic equation code above:
|
||||
|
||||
```kcl
|
||||
// Coefficients that define the quadratic
|
||||
a = 2
|
||||
b = 3
|
||||
c = 1
|
||||
|
||||
// The quadratic equation's discriminant
|
||||
delta = pow(b, exp = 2) - (4 * a * c)
|
||||
|
||||
// The two roots of the equation
|
||||
x0 = ((-b) + sqrt(delta)) / (2 * a)
|
||||
x1 = ((-b) - sqrt(delta)) / (2 * a)
|
||||
```
|
||||
|
||||
If you type `//`, any subsequent text on that line is a comment. It doesn't get executed like the rest of the code! It's just for other humans to read.
|
||||
|
||||
## The standard library
|
||||
|
||||
KCL comes built-in with functions for all sorts of common engineering problems -- functions to calculate equations, sketch 2D shapes, combine and manipulate 3D shapes, etc. The built-in KCL functions are called the _standard library_, because it's like a big library of code you can always use.
|
||||
|
||||
You can create your own functions too, but we'll save that for a future chapter. You can get pretty far just using the built-in KCL functions! We're nearly ready to do some actual CAD work, but we've got to learn one more essential KCL feature first.
|
||||
|
108
kcl-book/src/csg.md
Normal file
@ -0,0 +1,108 @@
|
||||
# Combining 3D solids
|
||||
|
||||
We've seen how to make a lot of different solids. You could transform a 2D shape into a 3D solid. From there, you can copy and transform that 3D solid by rotating, translating or rotating it. Now it's time to learn a third way to build 3D solids: by combining other 3D solids. This is sometimes called _constructive solid geometry_ and it's a very powerful tool for any serious mechanical engineering work.
|
||||
|
||||
## Constructive solid geometry
|
||||
|
||||
Remember in school, when you learned about Venn diagrams? How you can take the _union_, the _intersection_ or the _difference_ of two shapes? If you need a quick recap, here's a screenshot from [Wikipedia's article on boolean operations].
|
||||
|
||||

|
||||
|
||||
We can perform similar operations on 3D solids in KCL. Let's see how. Here's two cubes.
|
||||
|
||||
```kcl=two_cubes
|
||||
length = 20
|
||||
cubeGreen = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#229922")
|
||||
|
||||
cubeBlue = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> translate(x = 10, z = 10)
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#222299")
|
||||
```
|
||||
|
||||

|
||||
|
||||
That's what it looks like _before_ we apply any CSG operations. Now let's see what happens when we use KCL's [`union`], [`intersect`] and [`subtract`] functions on these. Firstly, let's do a union. This should create a new solid which combines both input solids.
|
||||
|
||||
```kcl=two_cubes_union
|
||||
length = 20
|
||||
cubeGreen = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#229922")
|
||||
|
||||
cubeBlue = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> translate(x = 10, z = 10)
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#222299")
|
||||
|
||||
// Boolean operations on the two cubes
|
||||
both = union([cubeGreen, cubeBlue])
|
||||
```
|
||||
|
||||

|
||||
|
||||
Of course, this [`union`] of our two cubes has the exact same dimensions and position as the two cubes. So it looks the exact same. What's the point of doing this? Well, for a start, we can use transforms like `appearance` or `rotate` on the single unified shape. Previously we needed to transform each part separately, which can get annoying. Now that it's a single shape, transformations will apply to the whole thing -- both the first cube's volume, and the second cube's.
|
||||
|
||||
**Note**: Instead of writing `union([cubeGreen, cubeBlue])` you can use the shorthand `cubeGreen + cubeBlue` or `cubeGreen | cubeBlue`. This is a nice little shorthand you can use if you want to.
|
||||
|
||||
Let's try an intersection. This combines both cubes, but leaves only the volume from where they overlapped.
|
||||
|
||||
|
||||
```kcl=two_cubes_intersection
|
||||
length = 20
|
||||
cubeGreen = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#229922")
|
||||
|
||||
cubeBlue = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> translate(x = 10, z = 10)
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#222299")
|
||||
|
||||
// Boolean operations on the two cubes
|
||||
both = intersect([cubeGreen, cubeBlue])
|
||||
```
|
||||
|
||||

|
||||
|
||||
This keeps only the small cube shape from where the previous two intersected. This is a new solid, so it can be transformed just like any other solid.
|
||||
|
||||
**Note**: Instead of writing `intersect([cubeGreen, cubeBlue])` you can use the shorthand `cubeGreen & cubeBlue`. This is a nice little shorthand you can use if you want to.
|
||||
|
||||
Lastly, let's try a `subtract` call:
|
||||
|
||||
```kcl=two_cubes_subtraction
|
||||
length = 20
|
||||
cubeGreen = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#229922")
|
||||
|
||||
cubeBlue = startSketchOn(XY)
|
||||
|> polygon(radius = length, numSides = 4, center = [0, 0])
|
||||
|> translate(x = 10, z = 10)
|
||||
|> extrude(length = length)
|
||||
|> appearance(color = "#222299")
|
||||
|
||||
// Boolean operations on the two cubes
|
||||
both = subtract(cubeGreen, tools=[cubeBlue])
|
||||
```
|
||||
|
||||

|
||||
|
||||
Note that the syntax for `subtract` is a little different. The first argument is the solid which will have some volume carved out. The second argument is a list of solids to cut out. You can think of these as "tools" -- you're basically passing tools of various shapes which can carve out special volumes.
|
||||
|
||||
**NOTE**: Currently only one tool can be passed in, but we're nearly finished supporting multiple tools here.
|
||||
|
||||
[Wikipedia's article on boolean operations]: https://en.wikipedia.org/wiki/Set_(mathematics)#Basic_operations
|
||||
[`intersect`]: https://zoo.dev/docs/kcl-std/intersect
|
||||
[`subtract`]: https://zoo.dev/docs/kcl-std/subtract
|
||||
[`union`]: https://zoo.dev/docs/kcl-std/union
|
208
kcl-book/src/declaring_functions.md
Normal file
@ -0,0 +1,208 @@
|
||||
# Functions and parametric design
|
||||
|
||||
+In mechanical engineering, _parametric design_ is a key tool that helps you avoid redundant work when you're designing the same object over and over again with slight tweaks. In software engineering, _functions_ are a key tool that help you avoid redundant work when you're designing the same software over and over again with slight tweaks.
|
||||
|
||||
That's right -- breaking a mechanical engineering project into several key parametric designs is basically the same as breaking a software engineering project into several key functions. KCL makes parametric design easy and convenient with functions. You'll declare functions to represent parametric designs, and you'll call those functions with specific arguments to produce specific designs with the right parameters. Let's see how.
|
||||
|
||||
## Function declaration
|
||||
|
||||
We briefly looked at function declarations when we covered pattern transforms. Let's write an example function declaration and analyze its parts.
|
||||
|
||||
```kcl
|
||||
fn add(a, b) {
|
||||
sum = a + b
|
||||
return sum
|
||||
}
|
||||
```
|
||||
|
||||
A function declaration has a few key parts. Let's look at each one, in the order they appear:
|
||||
|
||||
- The `fn` keyword
|
||||
- The function's _name_
|
||||
- Round parentheses `(` and `)`
|
||||
- Within those parentheses, a list of argument names
|
||||
- Curly brackets `{` and `}`
|
||||
- Within those brackets, KCL code, which may end with a `return` statement.
|
||||
|
||||
This function takes two arguments, `a` and `b`, adds them, and returns their sum as the function's output. When a function executes the `return` statement, it evaluates the expression after `return`, stops executing, and outputs that value. You can call our example function like this:
|
||||
|
||||
```kcl
|
||||
sum = add(a = 1, b = 2)
|
||||
```
|
||||
|
||||
Functions can also declare one *unlabeled* arg. If you do want to declare an unlabeled arg, it must be the first arg declared. When declaring an unlabeled arg, prefix it with `@`, like here:
|
||||
|
||||
```kcl
|
||||
// The @ indicates an argument can be used without a label.
|
||||
// Note that only the first argument can use @.
|
||||
fn increment(@x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
fn add(@x, delta) {
|
||||
return x + delta
|
||||
}
|
||||
|
||||
two = increment(1)
|
||||
three = add(1, delta = 2)
|
||||
```
|
||||
|
||||
## Mechanical engineering with functions
|
||||
|
||||
Let's use functions to build a parametric pipe flange. We can start with a specific design, with specific direct measurements. Then we'll learn how to parameterize it. Then we can easily make a lot of similar pipe flanges with different parameters.
|
||||
|
||||
Here's a specific model. It's got 8 unthreaded holes, each with a radius of 4, and the overall model has a radius of 60. It's 10mm thick.
|
||||
|
||||
```kcl=specific_flange
|
||||
holes = startSketchOn(XZ)
|
||||
|> circle(radius = 4, center = [50, 0])
|
||||
|> patternCircular2d(
|
||||
center = [0, 0],
|
||||
instances = 8,
|
||||
arcDegrees = 360,
|
||||
rotateDuplicates = true,
|
||||
)
|
||||
|
||||
base = startSketchOn(XZ)
|
||||
|> circle(radius = 60, center = [0, 0])
|
||||
|> subtract2d(tool = holes)
|
||||
|> extrude(length = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Its specific measurements, like number of holes, radius, thickness etc were chosen somewhat arbitrarily. What if we want to make another pipe flange in the future, with different measurements? We can turn this specific flange model into a parametric design by making it into a function. We'll define a function `pipe_flange` which takes in several parameters. Let's see:
|
||||
|
||||
```kcl=parametric_flange
|
||||
// Define a parametric flange
|
||||
fn flange(numHoles, holeRadius, radius, thickness) {
|
||||
holes = startSketchOn(XZ)
|
||||
|> circle(radius = holeRadius, center = [radius - holeEdgeGap, 0])
|
||||
|> patternCircular2d(
|
||||
center = [0, 0],
|
||||
instances = numHoles,
|
||||
arcDegrees = 360,
|
||||
rotateDuplicates = true,
|
||||
)
|
||||
|
||||
return startSketchOn(XZ)
|
||||
|> circle(radius = radius, center = [0, 0])
|
||||
|> subtract2d(tool = holes)
|
||||
|> extrude(length = thickness)
|
||||
}
|
||||
```
|
||||
|
||||
We can get our original flange by calling the parametric flange with the right parameters:
|
||||
```kcl
|
||||
// Call our parametric flange function, passing in specific parameter values, to make a specific flange.
|
||||
flange(
|
||||
numHoles = 8,
|
||||
holeRadius = 5,
|
||||
radius = 60,
|
||||
thickness = 10,
|
||||
holeEdgeGap = 10,
|
||||
)
|
||||
```
|
||||
|
||||
But we can also make a range of other flanges! Here's one:
|
||||
|
||||
```kcl
|
||||
flange(
|
||||
numHoles = 4,
|
||||
holeRadius = 15,
|
||||
radius = 60,
|
||||
thickness = 20,
|
||||
holeEdgeGap = 20,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
And let's try one more:
|
||||
|
||||
```kcl
|
||||
flange(
|
||||
numHoles = 20,
|
||||
holeRadius = 3,
|
||||
radius = 90,
|
||||
thickness = 20,
|
||||
holeEdgeGap = 15,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Replacing specific KCL code for a specific design with a parametric function gives you the flexibility to generate a lot of very similar designs, varying their parameters by passing in different arguments to suit whatever your project's requirements are.
|
||||
|
||||
## Repeating geometry with functions
|
||||
|
||||
Functions can also be used to avoid writing the same code over and over again, in a single model. In an earlier chapter we modeled three cubes in one scene, like this:
|
||||
|
||||
```kcl=cube_textures
|
||||
offset = 25
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|
||||
greenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
// The appearance call lets you set a color using hexadecimal notation.
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCubeShiny = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset * 2])
|
||||
|> extrude(length = 10)
|
||||
// You can also set the metalness and roughness, as percentages between 0 and 100.
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
This code works fine, but it's got one small problem. We're repeating the code for "sketch a cube" three times. This makes it a bit annoying to read, and also, if we want to tweak the cubes (making them larger, or rotating them), we'd have to update them in three different places. We could improve this code by making a function for the cube, and calling that function three times.
|
||||
|
||||
```kcl
|
||||
fn cube(offset) {
|
||||
return startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
}
|
||||
|
||||
greyCube = cube(offset = 0)
|
||||
|
||||
greenCube = cube(offset = 25)
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCubeShiny = cube(offset = 50)
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||
This code produces the exact same model as the above code, but it's shorter and easier to read. It's also more maintainable! If we wanted to change the cubes to be flatter, we only have to change one part of our code, instead of changing all three.
|
||||
|
||||
```kcl=three_short_cubes
|
||||
fn cube(offset) {
|
||||
return startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
// Change the extrude length from 10 to 2, shortening the cubes.
|
||||
|> extrude(length = 2)
|
||||
}
|
||||
|
||||
greyCube = cube(offset = 0)
|
||||
|
||||
greenCube = cube(offset = 25)
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCubeShiny = cube(offset = 50)
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
If we hadn't made the cube into a function, we would have had to change every extrude call separately. By putting the details of "what does a cube look like" in a single function, we make our code both more readable, and easier to change in the future.
|
||||
|
BIN
kcl-book/src/images/dynamic/basic_circle.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
kcl-book/src/images/dynamic/chamfered_cube.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
kcl-book/src/images/dynamic/circle.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
kcl-book/src/images/dynamic/circular_cubes_false.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
kcl-book/src/images/dynamic/circular_cubes_partway.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
kcl-book/src/images/dynamic/circular_cubes_true.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
kcl-book/src/images/dynamic/cube_eight_fillets.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
kcl-book/src/images/dynamic/cube_four_fillets.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
kcl-book/src/images/dynamic/cube_next_prev_fillets.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
kcl-book/src/images/dynamic/cube_next_prev_fillets_all_sides.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
kcl-book/src/images/dynamic/cube_no_fillets.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
kcl-book/src/images/dynamic/cube_one_fillet.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
kcl-book/src/images/dynamic/cube_spiral.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
kcl-book/src/images/dynamic/cube_textures.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
kcl-book/src/images/dynamic/cube_two_opposite_fillets.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
kcl-book/src/images/dynamic/custom_plane.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
kcl-book/src/images/dynamic/donut.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
kcl-book/src/images/dynamic/donut240.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
kcl-book/src/images/dynamic/donut_angle_units.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
kcl-book/src/images/dynamic/linear_pattern.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
kcl-book/src/images/dynamic/lines_units.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
kcl-book/src/images/dynamic/loft_basic.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
kcl-book/src/images/dynamic/loft_vd1.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
kcl-book/src/images/dynamic/loft_vd2.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
kcl-book/src/images/dynamic/parametric_flange.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
kcl-book/src/images/dynamic/path_for_sweep.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
kcl-book/src/images/dynamic/pattern2d.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
kcl-book/src/images/dynamic/pattern_sof.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
kcl-book/src/images/dynamic/reduce_comb.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
kcl-book/src/images/dynamic/reduce_polygon.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
kcl-book/src/images/dynamic/reduce_square.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
kcl-book/src/images/dynamic/rotated_cubes.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
kcl-book/src/images/dynamic/rotated_cubes_axis.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
kcl-book/src/images/dynamic/scaled_cubes.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
kcl-book/src/images/dynamic/semicircle.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
kcl-book/src/images/dynamic/sketch_on_chamfered_cube.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
kcl-book/src/images/dynamic/specific_flange.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
kcl-book/src/images/dynamic/sphere.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
kcl-book/src/images/dynamic/subtract2d_patterns.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
kcl-book/src/images/dynamic/swept_along_path.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
kcl-book/src/images/dynamic/three_map_cubes.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
kcl-book/src/images/dynamic/three_map_cubes_color.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
kcl-book/src/images/dynamic/three_offset_planes.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
kcl-book/src/images/dynamic/three_short_cubes.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
kcl-book/src/images/dynamic/translate_cubes.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
kcl-book/src/images/dynamic/triangle_for_sketching.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
kcl-book/src/images/dynamic/triangle_top_and_bottom_sketches.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
kcl-book/src/images/dynamic/triangle_with_cylinder_sketched.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
kcl-book/src/images/dynamic/two_cubes.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
kcl-book/src/images/dynamic/two_cubes_intersection.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
kcl-book/src/images/dynamic/two_cubes_subtraction.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
kcl-book/src/images/dynamic/two_cubes_union.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
kcl-book/src/images/dynamic/xform_chessboard.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
kcl-book/src/images/dynamic/xform_grid.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
kcl-book/src/images/static/1.png
Normal file
After Width: | Height: | Size: 767 KiB |
BIN
kcl-book/src/images/static/another_flange.png
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
kcl-book/src/images/static/boolean_2d_ops.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
kcl-book/src/images/static/first_triangle.png
Normal file
After Width: | Height: | Size: 703 KiB |
BIN
kcl-book/src/images/static/new_flange.png
Normal file
After Width: | Height: | Size: 368 KiB |
BIN
kcl-book/src/images/static/pill_2d.png
Normal file
After Width: | Height: | Size: 723 KiB |
BIN
kcl-book/src/images/static/pill_3d.png
Normal file
After Width: | Height: | Size: 785 KiB |
BIN
kcl-book/src/images/static/pill_sketch.png
Normal file
After Width: | Height: | Size: 837 KiB |
BIN
kcl-book/src/images/static/revolve_2d.png
Normal file
After Width: | Height: | Size: 729 KiB |
BIN
kcl-book/src/images/static/revolve_3d.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
kcl-book/src/images/static/revolve_3d_360.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
kcl-book/src/images/static/spiral.png
Normal file
After Width: | Height: | Size: 818 KiB |
BIN
kcl-book/src/images/static/triangle_closed.png
Normal file
After Width: | Height: | Size: 840 KiB |
BIN
kcl-book/src/images/static/triangle_open.png
Normal file
After Width: | Height: | Size: 867 KiB |
29
kcl-book/src/intro.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Introduction
|
||||
|
||||
Engineering is about solving problems, given some constraints. My name is Adam, and I'm a software engineer. I write code. Many of my colleagues at Zoo are hardware engineers. They design and build real-world objects. Some people see software and hardware engineering as two different disciplines. After all, software can't be touched or seen. It can't keep the two ends of a bridge together. And hardware -- everything from bridges to buildings to clamps to rockets -- can. Physical and software systems seem very separate. But that's not how I see it. Both coders and Formula 1 car designers are fundamentally _engineers_. We're both trying to solve difficult problems, with limited resources and various constraints. We're both trying to satisfy some requirement (like function) while minimizing some measurements (like cost or size) and still making the final result comprehensible (to our colleagues, to our users, or to our future self who revisits this project in two years).
|
||||
|
||||
Zoo started when Jess, a software engineer, called Jordan, an aerospace engineer, about her struggles with a really complicated CAD model. The two realized they had a lot more in common than they thought. They realized that if software and hardware engineers take the best parts of each other's practices and tools, we could get our jobs done quicker and with less stress. Every day at Zoo, we put bright engineers from all disciplines together and let them learn from each other.
|
||||
|
||||
These days, hardware engineers need to use software. The days of hand-drawing all your designs on a drafting table are over. But hardware engineers often find their software frustrating. Software engineers understand -- ranting about how much you hate software is a time-honored tradition among software engineers. The difference is, when software engineers don't like their tools, they know how to take them apart and make new ones. We can find the source code for our software, fork it, and add new features. Or we could even write our own version from scratch, that works exactly how we'd like it to. Unfortunately, when _hardware_ engineers don't like their software, they're stuck. They don't usually know enough programming to edit their engineering software. So they're at the mercy of some software engineer at a different company, who doesn't understand the problem well enough. Our goal at Zoo is to put the software and hardware engineers in the same room, so that when hardware engineers complain about their tool, the software people are listening and can quickly improve it. Experimenting with ideas from both worlds will be the key to modern, 21st century engineering.
|
||||
|
||||
KCL is one of the first successes from this hardware-software-engineer collaboration. We hope it will make CAD both easier to understand and more powerful. KCL, or the KittyCAD Language, is a programming language for CAD.
|
||||
|
||||
## Why a programming language?
|
||||
|
||||
Zoo's programmers -- experts in programming languages -- listened to Zoo's aerospace engineers, and concluded that what they needed was... a programming language! Surprise surprise. No, wait, don't run away aerospace engineers! It's not that bad, I promise!
|
||||
|
||||
Hardware engineers often shudder when I tell them we're building a programming language for CAD. I understand why! CAD is complicated. Programming is complicated. Simplifying CAD with programming is like solving your rampaging boar problem by introducing rampaging lions to control them. But we really believe in this approach. We've seen first-hand the benefits of code-driven CAD. We know other software has tried to combine them, but we think they started with three fundamental flaws:
|
||||
|
||||
1. The code has to be fundamental. You can't build a non-code CAD suite and then slap code on afterwards. Otherwise, there'll be gaps between these two halves -- things you can do in code but not in the "normal software", and vice-versa.
|
||||
2. Not everyone will want to code -- and that's OK. If you don't want to learn KCL, you don't have to! You can still use a traditional mouse-based, point-and-click workflow if you're more comfortable there. Every time you click a button, Zoo Design Studio is actually generating KCL under the hood. You've been writing KCL without knowing it! Or more accurately, telling the computer to write the KCL for you. If you decide to learn KCL later, you can open up your existing models and view the KCL.
|
||||
3. Reusing existing languages. JavaScript, C, Python etc are all great languages. But they were designed for software engineering, and the problems that software engineers solve. KCL is designed for engineering real world objects, not software. So it makes different choices. Existing languages require you to learn a bunch of little details that matter a lot to programmers, but aren't really important to mechanical engineers. KCL doesn't have any of those. Instead, it has built-in features that match how mechanical engineers think.
|
||||
|
||||
Learning to code takes work. Why bother learning KCL if you can just use the point-and-click UI instead? There's a few reasons.
|
||||
|
||||
Firstly, KCL lets you read the fundamental model underlying your designs. In normal CAD software, if you want to understand your model, you have to spin it around in the UI, look at different parts, maybe hide or show various faces that would block your view of its internals. This is because you never really access the model _directly_. Instead, you view a _rendering_ of the model. Feature trees help, but they only show a subset of the information connecting your model. KCL lets you directly read the exact same code that our CAD suite is executing. If you want to know why a hole has a certain diameter, you can just go to the line of code which defines that hole, and see where it gets its length. Is it a direct measurement, handwritten like `length = 2mm`? Is it a parameter from a parametric design? Is it the result of a calculation, like `length = totalHeight * 0.3`? Code makes it easy to see exactly where your measurements come from.
|
||||
|
||||
Secondly, KCL is the interface between human and computer. Programming languages aren't really built for computers. Computers use binary instructions like 1101010101010100000011. The first computers had to be programmed in binary, and coders would look up each instruction carefully in a huge reference manual to find which 0s and 1s each instruction needed. This was obviously very tedious, so programming languages were invented instead. Both humans and machines can read code like `let x = y + 3`. The human knows what it means, and the computer knows how to execute it.
|
||||
|
||||
Lastly, KCL is the interface between humans and other humans. Let's say you're collaborating on a CAD model with a coworker. They send you the latest revision of some part, and you open it up. What's changed? Impossible to tell. You'd have to open up the old revision and glance back-and-forth between the two to find the difference. This is _much_ easier when your model is stored as code! You can easily see the exact lines of code that changed -- green for new lines added, red for old lines removed, yellow for changes. And your coworker can leave comments in the code, so that anyone reading it understands exactly what has changed. As a bonus, this works even for solo engineers. If you want to see how your model has changed since last year, you can open up last year's file and run the same line-by-line visual comparison.
|
||||
|
||||
By learning KCL, you're developing a skill that can open up massive improvements to your design and engineering skills. It's a new skill, and like all new skills it takes time and practice to develop. But it's worth it. Code-driven CAD can become your superpower and let you simplify your designs _and_ design quicker. I'm excited to guide you on this journey. Let's get started.
|
125
kcl-book/src/map_reduce.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Iterating with map and reduce
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
Every programming language has a way to do _iteration_: to repeat the same task many times. Traditional programming languages like Python, JavaScript or C use loops. KCL doesn't have any loops, but we have something very similar: arrays, and two helper functions called [`map`] and [`reduce`]. Let's see how they can solve problems.
|
||||
|
||||
## Transforming arrays with map
|
||||
|
||||
The [`map`] function lets you transform an array by calling a function on every element. For example:
|
||||
|
||||
```kcl
|
||||
inputArray = [1, 2, 3, 4]
|
||||
fn squareNumber(@x) { return x * x }
|
||||
outputArray = map(inputArray, f = squareNumber)
|
||||
```
|
||||
|
||||
The [`map`] function takes an input array as its first argument, then a function (its label is abbreviated to just `f`). It calls the function on every element of the input array, and returns it. If you open the Variables pane, you'll see that outputArray is `[1, 4, 9, 16]`, just as we expected.
|
||||
|
||||
You can use `map` to create geometry too! For example, let's make 3 cubes, next to each other.
|
||||
|
||||
```kcl=three_map_cubes
|
||||
fn cube(@offset) {
|
||||
return startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
}
|
||||
|
||||
offsets = [0, 25, 50]
|
||||
cubes = map(offsets, f = cube)
|
||||
```
|
||||
|
||||

|
||||
|
||||
We created an array of offsets, then called the `cube` function on each offset in the array. The final result is an array of cubes. Calling the `cube` function drew the three cubes, each at their own offset.
|
||||
|
||||
So far so good. But this is basically just a 3D pattern. We can make this more interesting by making each cube a different color. Instead of an array of offsets, we'll store an array of offsets _and colors_. To do this, we'll make a KCL _object_. An object has multiple properties, each with its own label and value. For example:
|
||||
|
||||
```kcl
|
||||
myObject = {
|
||||
offset = 25,
|
||||
color = "#00ff00",
|
||||
}
|
||||
```
|
||||
|
||||
This object has two fields, `offset` and `color`. You could access them by calling `myObject.offset` and `myObject.color`. Let's see how we can use this with `map`:
|
||||
|
||||
```kcl=three_map_cubes_color
|
||||
fn cube(@params) {
|
||||
offset = params.x
|
||||
color = params.color
|
||||
return startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
|> appearance(color = color)
|
||||
}
|
||||
|
||||
offsets = [
|
||||
{ x = 0, color = "#99ff99" }, // Dark green
|
||||
{ x = 25, color = "#00ff00" }, // Bright green
|
||||
{ x = 50, color = "#002200" }, // Pale green
|
||||
]
|
||||
|
||||
map(offsets, f = cube)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Remember, `map` takes in an array, and outputs an array. The arrays always have the same length. Item `x` in the input array will be `f(x)` in the output array, where `f` is whichever function you pass in.
|
||||
|
||||
### Anonymous functions
|
||||
|
||||
It can get annoying defining a new function every time you want to use `map`. For instance, in the earlier example where we defined a `fn squareNumber` to use in a `map` -- is that really necessary? If you have a lot of `map` calls, you'll slowly find your code becoming littered with tiny functions that you only use in a `map`.
|
||||
|
||||
KCL supports a nice little feature that can simplify this: _anonymous functions_. They're functions that don't have a name. You declare them where you need them, they're passed into `map`and they aren't available after. Let's have a look:
|
||||
|
||||
```kcl
|
||||
inputArray = [1, 2, 3, 4]
|
||||
outputArray = map(inputArray, f = fn (@x) { return x * x })
|
||||
```
|
||||
|
||||
In this variation, we're passing in an _anonymous function_ as the argument `f` of `map`. Just like before, it takes a single input argument `x`, squares it, and returns it. It should produce the exact same output as the earlier example with a function named `squareNumber`.
|
||||
|
||||
You can choose to use either named or anonymous functions with `map`. Neither is better or worse, you can use whichever you prefer. Generally, if a function is only a single line long, and you're only going to call it once (in a `map` or something similar), then you should consider making it anonymous and passing it as an argument directly.
|
||||
|
||||
## Consuming arrays with reduce
|
||||
|
||||
The `map` function lets you iterate over an array, producing another array with the same length. But what if you don't want to get an array out? For example, what if you want to sum an array, or find the average?
|
||||
|
||||
The answer is: use the [`reduce`] function. This function, like `map`, takes an array and a function, then it calls the function on every element in the array. The difference is:
|
||||
|
||||
- In `map`, the function argument `f` takes a single arg: the array's item being processed, often called `i`.
|
||||
- In `reduce`, the function argument `f` takes _two_ args: the array's item being processed `i` as well as a second value that _accumulates_ across the array. It's called `accum`, short for reduce.
|
||||
|
||||
Let's see an example:
|
||||
|
||||
```kcl
|
||||
inputArray = [1, 2, 3, 4]
|
||||
sum = reduce(inputArray, initial = 0, f = fn(@i, accum) => { return i + accum })
|
||||
```
|
||||
|
||||
If you open `sum` in the Variables pane, you'll see it's 10, as we expect. How does this work? Let's break it down and see what happens in each step of the `reduce`.
|
||||
|
||||
- The `reduce` starts. It sets `accum` to its initial value, which is the `initial = 0` arg. So, `accum` starts at 0.
|
||||
- Reduce starts iterating over the array.
|
||||
- The first item is `1`. Reduce calls `f`, passing `i=1` and `accum=0`. Then `f` returns `1+0`, or 1. This becomes the new value of `accum`.
|
||||
- The next item is `2`. Reduce calls `f`, passing `i=2` and `accum=1`. Then `f` returns `2+1`, or 3. This becomes the new value of `accum`.
|
||||
- The next item is `3`. Reduce calls `f`, passing `i=3` and `accum=3`. Then `f` returns `3+3`, or 6. This becomes the new value of `accum`.
|
||||
- The next item is `4`. Reduce calls `f`, passing `i=4` and `accum=6`. Then `f` returns `4+6`, or 10. This becomes the new value of `accum`.
|
||||
- There's no more array items to handle, so reduce returns the last accumulated value, 10.
|
||||
|
||||
That's how reduce can take a long list of items and _reduce_ it to a single item, _accumulating_ the answer as it goes through the array.
|
||||
|
||||
What are some other things we can do with `reduce`? We could calculate the product of an array:
|
||||
|
||||
```kcl
|
||||
reduce(inputArray, initial = 1, f = fn(@i, accum) { return i * accum})
|
||||
```
|
||||
|
||||
In the next chapter we'll cover one of the most powerful uses for `reduce`: dynamically building up geometry.
|
||||
|
||||
[`map`]: https://zoo.dev/docs/kcl-std/map
|
||||
[`reduce`]: https://zoo.dev/docs/kcl-std/reduce
|
||||
|
1
kcl-book/src/modules.md
Normal file
@ -0,0 +1 @@
|
||||
# Modules
|
258
kcl-book/src/patterns.md
Normal file
@ -0,0 +1,258 @@
|
||||
# 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.
|
||||
|
||||
```kcl=linear_pattern
|
||||
cylinders = startSketchOn(XY)
|
||||
|> circle(radius = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> patternLinear3d(instances = 4, distance = 10, axis = [1, 0, 0])
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`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:
|
||||
|
||||
```kcl=circular_cubes_false
|
||||
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,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Here, the center of the pattern is [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:
|
||||
|
||||
```kcl=circular_cubes_true
|
||||
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,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Of course, if we change the `arcDegrees` argument, we could pattern around only part of the circle instead. Let's do two thirds of the circle:
|
||||
|
||||
```kcl=circular_cubes_partway
|
||||
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,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
You can use patterns and sketch on face together, patterning an extrusion upon some base.
|
||||
|
||||
```kcl=pattern_sof
|
||||
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.
|
||||
|
||||
```kcl=xform_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 set `i` to 1, the second copy will set `i` to 2, etc etc. The argument `i` is prefixed with `@` to indicate it's this function's special first unlabeled arg, so if you call it, you'd call it like `grid(1)` or `grid(2)`, not `grid(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:
|
||||
|
||||

|
||||
|
||||
We can transform each replica in other ways, too. For example, we can skip a replica altogether! Let's make a chessboard pattern, where we skip every second tile.
|
||||
|
||||
```kcl=xform_chessboard
|
||||
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!
|
||||
|
||||

|
||||
|
||||
Here's another example, with some different transform properties being set.
|
||||
|
||||
```kcl=cube_spiral
|
||||
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 = 100, 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.
|
||||
|
||||

|
||||
|
||||
The transform functions we've used so far each return a single transform. But if you'd like, they can return an array of transforms. Each transform in the array will get executed in order. This is helpful for simplifying some of your math calculations. Sometimes it's easier to formulate a transformation as a rotate, then a translate, then rotating back, rather than trying to calculate the perfect translation all at once.
|
||||
|
||||
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:
|
||||
|
||||
```kcl=pattern2d
|
||||
manyCircles = startSketchOn(XZ)
|
||||
|> circle(radius = 4, center = [50, 0])
|
||||
|> patternCircular2d(
|
||||
center = [0, 0],
|
||||
instances = 12,
|
||||
arcDegrees = 360,
|
||||
rotateDuplicates = true,
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Now, you could use these 2D patterns as the basis for 3D solids, by extruding or revolving them. You can see this by adding the line `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.
|
||||
|
||||
```kcl=subtract2d_patterns
|
||||
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)
|
||||
```
|
||||
|
||||

|
||||
|
||||
This could be done with CSG, but it's faster to produce the 2D sketch you want, then do a simple extrude, rather than doing the extrude and then many CSG operations. Full 3D CSG operations are mathematically difficult to calculate compared to simple 2D operations, so if you see the chance to use a simple [`subtract2d`], you should consider it.
|
||||
|
||||
[`patternLinear3d`]: https://zoo.dev/docs/kcl-std/patternLinear3d
|
||||
[`patternLinear2d`]: https://zoo.dev/docs/kcl-std/patternLinear2d
|
||||
[`patternCircular3d`]: https://zoo.dev/docs/kcl-std/patternCircular3d
|
||||
[`patternCircular2d`]: https://zoo.dev/docs/kcl-std/patternCircular2d
|
||||
[`patternTransform`]: https://zoo.dev/docs/kcl-std/patternTransform
|
||||
[`patternTransform2d`]: https://zoo.dev/docs/kcl-std/patternTransform2d
|
||||
[`rem`]: https://zoo.dev/docs/kcl-std/functions/std-math-rem
|
||||
[`floor`]: https://zoo.dev/docs/kcl-std/functions/std-math-floor
|
||||
[`subtract2d`]: https://zoo.dev/docs/kcl-std/subtract2d
|
99
kcl-book/src/pipelines.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Pipeline syntax
|
||||
<!-- toc -->
|
||||
|
||||
In the previous chapter we learned how to call functions: you write the function's name, then give its inputs in parentheses, like this:
|
||||
|
||||
```kcl
|
||||
x = pow(2, exp = 2)
|
||||
```
|
||||
|
||||
What if you want to repeatedly call a function, then call another function on that output? Here's an example:
|
||||
|
||||
```kcl
|
||||
sqrt(sqrt(sqrt(64)))
|
||||
```
|
||||
|
||||
We find the square root of 64, then pass its output as the input to another square root call. And another. And another. Eventually we've found the eighth root of 64.
|
||||
|
||||
This is pretty hard to read! We could make it more readable by breaking it up into single calls and assigning each to its own variable, like this:
|
||||
|
||||
```kcl
|
||||
x = 64
|
||||
y = sqrt(x)
|
||||
z = sqrt(y)
|
||||
w = sqrt(z)
|
||||
```
|
||||
|
||||
But then we have to think of meaningful names, and add a lot of variables. Now the Variables pane shows all these intermediate variables like `y` and `z`. Sometimes that's helpful, but sometimes it can be distracting.
|
||||
|
||||
Passing the output of a function into another function's input is a _very_ common task in KCL code. So, KCL has a nice little feature for simplifying this common pattern. It's called a _pipeline_. Let's rewrite the above using pipeline syntax:
|
||||
|
||||
|
||||
```kcl
|
||||
x = 64
|
||||
w = sqrt(x)
|
||||
|> sqrt(%)
|
||||
|> sqrt(%)
|
||||
```
|
||||
|
||||
What's going on here? Basically, if you call two functions like `g(f(x))` you could rewrite it as `f(x) |> g(%)`. Whatever is to the left of the `|>` gets calculated, then passed into the function on the right of `|>`. The `%` symbol basically means "use whatever was to the left of `|>`". The `|>` is basically a triangle pointing to the right, showing that the data on the left flows into the function on the right. You can think of it like an assembly line in a factory, moving parts (data) between different machines (functions) using a conveyor belt (the `|>` symbol).
|
||||
|
||||
Let's see another example. If you take a number's square root, and then square it again, it should give you the original number back. Let's test that.
|
||||
|
||||
|
||||
```kcl
|
||||
x = 64
|
||||
xRoot = sqrt(x)
|
||||
shouldBeX = pow(xRoot, exp = 2)
|
||||
```
|
||||
|
||||
Let's rewrite this using pipelines:
|
||||
|
||||
```kcl
|
||||
x = 64
|
||||
shouldBeX = sqrt(x)
|
||||
|> pow(%, exp = 2)
|
||||
```
|
||||
|
||||
## Implicit %
|
||||
|
||||
All those %s can be a bit annoying to read. Remember how some KCL functions declare a special unlabeled first argument? If a function uses the special unlabeled argument, then that argument will default to %. Basically, if you use these functions in a pipeline, you can omit the % and KCL will insert the % for you.
|
||||
|
||||
In other words, these two programs are equivalent:
|
||||
|
||||
```kcl
|
||||
x = 8
|
||||
|> pow(%, exp = 2)
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```kcl
|
||||
x = 8
|
||||
|> pow(exp = 2) // No % needed.
|
||||
```
|
||||
|
||||
`x` equals 64 in both these programs.
|
||||
|
||||
Let's see another example. We could simplify this program:
|
||||
```kcl
|
||||
x = 64
|
||||
w = sqrt(x)
|
||||
|> sqrt(%)
|
||||
|> sqrt(%)
|
||||
```
|
||||
|
||||
as
|
||||
|
||||
```kcl
|
||||
x = 64
|
||||
w = sqrt(x)
|
||||
|> sqrt()
|
||||
|> sqrt()
|
||||
```
|
||||
|
||||
Both programs work the exact same -- the first unlabeled argument in `sqrt` isn't given, so it defaults to %, i.e. the left-hand side of the |> symbol. This makes your code a bit cleaner and easier to read.
|
||||
|
||||
With that, you've learned the basics of KCL. You know how to declare data in variables, compute new data by calling functions, and join many functions together (either using pipelines or new variables). We're ready to get into mechanical engineering. In the next chapter we'll start looking at how KCL functions can define geometric shapes for your designs and models.
|
||||
|
||||
|
124
kcl-book/src/reduce_geometry.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Reduce and geometry
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
The `reduce` function lets us iterate over an array, consuming its contents and _reducing_ them down to one single item. Reduce is a very powerful, flexible tool. It can be complex too, but that complexity lets us do some very interesting things.
|
||||
|
||||
For example: how would you write a KCL function that produces an n-sided polygon? This is an ambitious project, so let's start with something simpler, and build back up to an n-sided polygon.
|
||||
|
||||
|
||||
## Sketching a square with reduce
|
||||
|
||||
Can we use `reduce` to make a `square` function, first? Once we've done that, we can make a parametric `sketchPolygon` function that works like `square` when the number of sides is 4, but can just as easily produce hexagons, octagons, triangles, etc.
|
||||
|
||||
```kcl=reduce_square
|
||||
fn square(sideLength) {
|
||||
emptySketch = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
angle = 90
|
||||
fn addOneSide(@i, accum) {
|
||||
return angledLine(accum, angle = i * angle, length = sideLength)
|
||||
}
|
||||
return reduce([1..4], initial = emptySketch, f = addOneSide)
|
||||
|> close()
|
||||
}
|
||||
|
||||
square(sideLength = 10)
|
||||
```
|
||||
|
||||
What's going on here? Let's break it down. We declare `fn square` which takes one argument, the `sideLength`. We create an initial empty sketch (at [0, 0] on the XY plane), and declare that the angle is 90.
|
||||
|
||||
Next, we declare a `fn addOneSide`. It takes in two arguments: `i`, which represents the index of which side we're currently adding, and `accum`, which is the sketch we're adding it to. This function adds one angled line to the sketch. The line's side is whatever side length was given, and its angle is 90 times `i`. So, the first line will have an angle of 90, the second 180, the third 270, and the last 360.
|
||||
|
||||
Then we call `reduce`, passing in the array [1, 2, 3, 4], setting the initial accumulator value to the empty sketch we started above, and calling `addOneSide` every time the reduce handles an array item. When reduce runs, it:
|
||||
|
||||
- Starts `accum` as the empty sketch
|
||||
- Handles the first item, `i = 1`, calls `addOneSide`, which takes the previous accumulated sketch (currently empty) and adds an angled line at 90 degrees. This becomes the next accumulated sketch.
|
||||
- Handles the second item, `i = 2`, calls `addOneSide`, which takes the previous accumulated sketch (with a single line) and adds an angled line at 180 degrees. This becomes the next accumulated sketch.
|
||||
- For `i = 3`, it takes the accumulated sketch with two lines, and adds a third line, similar to the previous step.
|
||||
- For `i = 4`, it takes the accumulated sketch with three lines, and adds a fourth line, similar to the previous step.
|
||||
|
||||
Thus it builds up a square.
|
||||
|
||||

|
||||
|
||||
## Sketching a parametric polygon with reduce
|
||||
|
||||
OK! We've seen how to use `reduce` to add lines to an empty sketch. We're ready to make our polygon function. Although, KCL already has a `polygon` function in the standard library. So, to avoid clashing with the existing name, we'll call ours `sketchPolygon`.
|
||||
|
||||
We can start with our `square` function and generalize it. First, we'll add an argument for the number of lines.
|
||||
|
||||
```kcl
|
||||
fn sketchPolygon(@numLines, sideLength) {
|
||||
}
|
||||
```
|
||||
|
||||
We can use the same initial empty sketch. We'll have to change `angle`, because it won't be 90 anymore. The angle now depends on how many edges the shape has:
|
||||
|
||||
```kcl
|
||||
angle = 360 / numLines
|
||||
```
|
||||
|
||||
And lastly, our `reduce` call will need to take an array of numbers from 1 to `numLines`, not 1 to 4. So we'll use `[1..numLines]` as the first argument to `reduce`.
|
||||
|
||||
Let's put all that together:
|
||||
|
||||
```kcl=reduce_polygon
|
||||
fn sketchPolygon(@numLines, sideLength) {
|
||||
initial = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
angle = 360 / numLines
|
||||
fn addOneSide(@i, accum) {
|
||||
return angledLine(accum, angle = i * angle, length = sideLength)
|
||||
}
|
||||
finished = reduce([1..numLines], initial = initial, f = addOneSide)
|
||||
return finished |> close()
|
||||
}
|
||||
|
||||
sketchPolygon(7, sideLength = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Reduce can be a very powerful tool for repeating paths in a sketch. We hope to simplify this in the future. It's easy to dynamically repeat 2D shapes or 3D solids with `pattern2D` and `pattern3D`, so we hope to add a `pattern1D` eventually, so that these complicated reduces won't be necessary. Until then, reduce can be a good way to implement tricky functions like `sketchPolygon`.
|
||||
|
||||
## Repeating geometry with reduce
|
||||
|
||||
Let's look at another way to use reduce. Say you're modeling a comb, with a parametric number of teeth. We can use `reduce` to solve this again:
|
||||
|
||||
```kcl=reduce_comb
|
||||
fn comb(teeth, sideLength) {
|
||||
toothAngle = 80
|
||||
handleHeight = 4
|
||||
initial = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|
||||
// Sketches a single comb tooth
|
||||
fn addTooth(@i, accum) {
|
||||
// Line going up
|
||||
return angledLine(accum, angle = toothAngle, length = sideLength)
|
||||
// Line going down
|
||||
|> angledLine(angle = -toothAngle, length = sideLength)
|
||||
}
|
||||
allTeeth = reduce([1..teeth], initial = initial, f = addTooth)
|
||||
finalComb = allTeeth
|
||||
// Add the handle: a line down, across, and back up to the start.
|
||||
|> yLine(length = -handleHeight)
|
||||
|> xLine(endAbsolute = 0)
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
return finalComb
|
||||
}
|
||||
|
||||
comb(teeth = 10, sideLength = 10)
|
||||
```
|
||||
|
||||
We write a function `addTooth` which adds a tooth (going up, then back down) to a sketch. Using `reduce`, we can call that function `teeth` times. Each time, the new tooth gets appended to the end of the sketch path. Once we've drawn all the teeth, we draw a simple handle leading back to the start.
|
||||
|
||||

|
||||
|
||||
To wrap up, [`reduce`] is a powerful way to make parametric designs, repeating geometric features as many times as you need. You can design parametric polygons with a variable number of sides, or repeat geometry linearly (like we did for our comb). You can even make parametric gears, take a look at the [KCL samples] for examples.
|
||||
|
||||
[`map`]: https://zoo.dev/docs/kcl-std/map
|
||||
[`reduce`]: https://zoo.dev/docs/kcl-std/reduce
|
||||
[KCL samples]: https://zoo.dev/docs/kcl-samples/gear
|
152
kcl-book/src/sketch2d.md
Normal file
@ -0,0 +1,152 @@
|
||||
# Sketching 2D shapes
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
Let's use KCL to sketch some basic 2D shapes. **Sketching** is a core workflow for mechanical engineers, designers, and hobbyists. The basic steps of sketching are:
|
||||
|
||||
1. Choose a plane to sketch on
|
||||
2. Start sketching at a certain point
|
||||
3. Draw a line from the current point to somewhere
|
||||
4. Add new lines, joining on from the previous lines
|
||||
5. Eventually, one line loops back to the starting point.
|
||||
6. Close the sketch, creating a 2D shape.
|
||||
|
||||
You can do each of these steps in KCL. Let's see how!
|
||||
|
||||
## Your first triangle
|
||||
|
||||
Let's sketch a really simple triangle. We'll sketch a right-angled triangle, with side lengths 3 and 4.
|
||||
|
||||
Just copy this code into the KCL editor:
|
||||
|
||||
|
||||
```kcl
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [3, 0])
|
||||
|> line(end = [0, 4])
|
||||
|> line(endAbsolute = [0, 0])
|
||||
|> close()
|
||||
```
|
||||
|
||||
Your screen should look something like this:
|
||||
|
||||

|
||||
|
||||
Congratulations, you've sketched your first triangle! Rendering your first triangle is a [big deal in graphics programming](https://rampantgames.com/blog/?p=7745), and sketching your first triangle is a big deal in KCL.
|
||||
|
||||
Let's break this code down line-by-line and see how it corresponds to each step of sketching from above. Note that each step in creating this triangle uses the pipeline syntax `|>`. This means every function call is being piped into the next function call.
|
||||
|
||||
### 1: Choose a plane
|
||||
|
||||
In KCL, there's six basic built-in planes you can use: XY, YZ, XZ, and negative versions of each (-XY, -YZ and -XZ). You can use one of these standard planes, or define your own (we'll get to that later). Those six standard planes can be used just like normal variables you define, except they're pre-defined by KCL in its standard library. You can pass them into functions, like the [`startSketchOn`] function. So, line 1, `startSketchOn(XY)` is where you choose a plane, and start sketching on it.
|
||||
|
||||
`startSketchOn` takes one argument, the plane to sketch on. It's the special unlabeled first parameter. We'll go over some other planes you can sketch on in the chapter about [sketch on face].
|
||||
|
||||
### 2: Start sketching
|
||||
|
||||
Sketches contain profiles -- basically, a sequence of lines, laid out top-to-tail (i.e. one line starts where the previous line ends). We have to start the profile somewhere, so we use `startProfile(at = [0, 0])`. The [`startProfileAt`] takes two parameters:
|
||||
|
||||
1. The sketch we're adding a profile with. This is one of those special unlabeled first parameters, so we don't need a label. We're setting it to the sketch from `startSketchOn(XY)`, which is being piped in via the `|>`. If you don't set this first parameter, it defaults to `%`, i.e. the previous pipeline expression. And that's exactly what we want! So we're leaving it unset.
|
||||
2. The `at` parameter indicates where the profile starts. For this example, we'll start at the origin of the XY plane, i.e. the point `[0, 0]`.
|
||||
|
||||
### 3: Add paths
|
||||
|
||||
A profile is a sequence of paths. A path is some sort of curve between two points, possibly straight lines, circular arcs, parabolae, or something else. For this triangle, we're adding 3 paths, which are all straight lines. The [`line`] call says to draw a line starting at the previous end point. Currently, this is `[0, 0]` from the `startProfileAt` call. So this line starts at `[0, 0]`. Where does it end? Well, the `line` call says that `end = [3, 0]`, which means "extend this line 3 units along the X axis, and 0 units along the Y axis". This is a _relative_ distance, because it's telling you how far to move from the previous point. So, this line goes from `[0, 0]` to `[3, 0]`.
|
||||
|
||||
### 4: Add more lines, joining on from previous lines.
|
||||
|
||||
The next call is `line(end = [0, 4])`. It draws a line from the previous line's end (`[3, 0]`), extending a distance of 0 along X and 4 along Y. So it goes from `[3, 0]` to `[3, 4]`.
|
||||
|
||||
### 5: Join back to the start
|
||||
|
||||
Our third line heads back to the start of the profile, i.e. `[0, 0]`. We do this by calling `line(endAbsolute = [0, 0])`. Note that this uses `endAbsolute =`, not `end =` like the previous lines. The `end =` arguments were _relative_ distances: they said how far away the new point is, along both X and Y axes, from the previous point. This one is different: this is an _absolute_ point, not a _relative_ distance. The array `[0, 0]` isn't saying to move 0 along X and 0 along Y. It's saying, draw a line that ends at the specific point `[0, 0]`, i.e. the origin of the plane.
|
||||
|
||||
Because this is the same point that our profile starts at, this line has looped our profile back to its start.
|
||||
|
||||
If we stopped our program here, you could see all three lines:
|
||||
|
||||

|
||||
|
||||
Note that the _relative_ lines (i.e. the first two line calls, with `end =`) have arrows showing where they're going. This last line, which ends at an _absolute_ point, does not.
|
||||
|
||||
### 6: Close the sketch
|
||||
|
||||
The last function being called is [`close`]. It takes one argument, the sketch to close. As in the previous functions, it's an unlabeled first parameter, so you could write `close(%)`, but `close()` will do the exact same thing.
|
||||
|
||||
Once we add the `close()` call, the rendering changes from just 3 lines (like in the second image in this page) to a filled-in shape (like in the first image on this page).
|
||||
|
||||
## Enhancements
|
||||
|
||||
This code totally achieved our goal: it sketches a right-angled triangle with sides of length 3 and 4. Mission accomplished.
|
||||
|
||||
Of course, in programming, there's usually several different ways to achieve a goal. KCL is no different! Let's look at some different ways we could have sketched this shape.
|
||||
|
||||
### Closing shapes
|
||||
|
||||
One important principle in programming is "don't repeat yourself" (DRY). Look back at this code: it uses the point `[0, 0]` twice. Once when we start the sketch, and once when we close the sketch. There's nothing necessarily wrong with this, but if you want to change the triangle later, you'll have to change this in two different places. And if you make a typo in one of the places, the model will break, because the sketch will be starting and finishing at a different point. This program doesn't have a bug currently, but by repeating this value twice, we introduce a _potential_ bug in the future. I'd call this program _brittle_ -- it's not broken, but it could break in the future. If we could define the point `[0, 0]` just once, the program would be more resilient, i.e. less likely to break if you change something in the future.
|
||||
|
||||
Here's a few ways to make this code less repetitive, less brittle, and DRY-er.
|
||||
|
||||
Firstly, you could replace [0, 0] with a variable like `start`, and use it in both places.
|
||||
|
||||
|
||||
```kcl
|
||||
start = [0, 0]
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = start)
|
||||
|> line(end = [3, 0])
|
||||
|> line(end = [0, 4])
|
||||
|> line(endAbsolute = start)
|
||||
|> close()
|
||||
```
|
||||
|
||||
Next, we could use a helper function [`profileStart`] instead.
|
||||
|
||||
```kcl
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [3, 0])
|
||||
|> line(end = [0, 4])
|
||||
|> line(endAbsolute = profileStart())
|
||||
|> close()
|
||||
```
|
||||
|
||||
The `profileStart` function takes in the current profile, and returns its start value. It takes a single unlabeled parameter, which we're setting to % (the left-hand side of the |>). Like always, if the special unlabeled argument is set to %, you can just omit the %, because that's the default.
|
||||
|
||||
### X and Y lines
|
||||
|
||||
The first line of our triangle is parallel to the X axis, and the second line is parallel to the Y axis. This means we could simplify our code somewhat by using the [`xLine`] and [`yLine`] functions:
|
||||
|
||||
```kcl
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = 3)
|
||||
|> yLine(length = 4)
|
||||
|> line(endAbsolute = profileStart())
|
||||
|> close()
|
||||
```
|
||||
|
||||
`xLine` takes an unlabeled first parameter for the sketch (which, as before, we're setting to % and can therefore omit) and then a `length` parameter, which tells KCL to draw a flat line, parallel to the X axis, with the given length. Basically,`xLine(length = n)` is a neater way to write a horizontal line like `line(end = [n, 0])`. You can use whichever one you prefer. The `yLine` function works the same way, but for vertical lines.
|
||||
|
||||
These examples use _relative_ xLine and yLine -- i.e. lines that end a certain _distance away from the previous point_. If you want to instead draw a line to a specific point along the X axis (like `x = 3`), you could use `xLine(endAbsolute = 3)`.
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
We've written our first triangle. We learned:
|
||||
|
||||
- Sketches are on some plane, and KCL includes standard planes XY, YZ and XZ (and their negative versions, which point the third axis in the opposite direction).
|
||||
- Sketches contain profiles, which are made of sequential paths. In our example, there's one profile, a triangle, made of three paths (3 straight lines).
|
||||
- Lines start at the end of the previous point (the first line starts at the `startProfile(at=)` point)
|
||||
- Lines can end either a certain distance away along X and Y (a _relative_ end), or at a particular point along the plane (an _absolute_ end)
|
||||
- The `close` function turns a sequence of paths that form a loop into a single 2D shape.
|
||||
|
||||
[`close`]: https://zoo.dev/docs/kcl/close
|
||||
[`line`]: https://zoo.dev/docs/kcl/line
|
||||
[`profileStart`]: https://zoo.dev/docs/kcl/profileStart
|
||||
[`startProfileAt`]: https://zoo.dev/docs/kcl/startProfileAt
|
||||
[`startSketchOn`]: https://zoo.dev/docs/kcl/startSketchOn
|
||||
[`xLine`]: https://zoo.dev/docs/kcl/xLine
|
||||
[`yLine`]: https://zoo.dev/docs/kcl/yLine
|
||||
[sketch on face]: /sketch_on_face.html
|
72
kcl-book/src/sketch2d_curves.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Sketching curved lines
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
In the previous chapter, we sketched a basic triangle. In this chapter, we'll look at some more interesting kinds of sketches you can do, using more interesting kinds of paths.
|
||||
|
||||
## Pills
|
||||
|
||||
Let's sketch a pill shape, like a rectangle but with rounded edges. We can use tangential arcs for this. The [`tangentialArc`] function sketch a curved line -- specifically, an arc, or a subset of a circle -- starting from the previous line's end. It draws it at a smooth angle from the previous line, i.e. _tangent_ to the previous line.
|
||||
|
||||
|
||||
```kcl
|
||||
height = 4
|
||||
width = 8
|
||||
startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = width)
|
||||
|> tangentialArc(end = [0, height])
|
||||
|> xLine(length = -width)
|
||||
|> tangentialArc(endAbsolute = profileStart())
|
||||
```
|
||||
|
||||
It should look like this:
|
||||
|
||||

|
||||
|
||||
Let's analyze this! It looks very similar to the triangle we sketched previously, but we're using `tangentialArc`. You can see it takes a relative `end`, i.e. an X distance and Y distance to move from the current point. It draws a nice smooth arc there.
|
||||
|
||||
We wrote this arc using `end`, i.e. an X and Y distance. But we could have defined this arc differently, using a `radius` and `angle` instead. You can replace the `tangentialArc(end = [0, height])` with `tangentialArc(angle = 180, radius = height)` instead, and it should draw the same thing.
|
||||
|
||||
The second `tangentialArc` call takes an absolute point. We tell it to draw an arc from the current point to the start of the profile. This should remind you of how straight lines can use either `end` (relative) or `endAbsolute`.
|
||||
|
||||
## Spirals
|
||||
|
||||
We can use tangential arcs to make a spiral too.
|
||||
|
||||
|
||||
```kcl
|
||||
height = 100
|
||||
startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> tangentialArc(angle = 180, radius = height)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.1)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.2)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.3)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.4)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.5)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.6)
|
||||
|> tangentialArc(angle = 180, radius = height * 1.7)
|
||||
```
|
||||
|
||||
It should look like this:
|
||||
|
||||

|
||||
|
||||
This works because each tangentialArc is drawing half a circle, away from the previous arc, and the circle is getting slightly larger each time.
|
||||
|
||||
## Circles
|
||||
|
||||
And lastly, let's look at the humble circle.
|
||||
|
||||
```kcl=basic_circle
|
||||
startSketchOn(XY)
|
||||
|> circle(center = [0, 0], radius = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`circle`] call takes `center` and `radius` arguments. Note that `circle` closes itself without any need for a `close()` call. That's because a circle is inherently closed -- it always starts and ends its own profile.
|
||||
|
||||
[`tangentialArc`]: <https://zoo.dev/docs/kcl/tangentialArc>
|
||||
[`circle`]: <https://zoo.dev/docs/kcl-std/functions/std-sketch-circle>
|
258
kcl-book/src/sketch3d.md
Normal file
@ -0,0 +1,258 @@
|
||||
# Modeling 3D shapes
|
||||
<!-- toc -->
|
||||
|
||||
Previous chapters covered designing 2D shapes. Now it's time to design 3D shapes!
|
||||
|
||||
3D shapes are usually made by adding depth to a 2D shape. There are two common ways engineers do this: by extruding or revolving 2D shapes into 3D. There's some less common ways too, including sweeps and lofts. In this chapter, we'll go through each of these! Let's get started with the most common method: extruding.
|
||||
|
||||
## Extrude
|
||||
|
||||
Extruding basically takes a 2D shape and pulls it up, stretching it upwards into the third dimension. Let's start with our existing 2D pill shape from the previous chapter:
|
||||
|
||||
```kcl
|
||||
height = 4
|
||||
width = 8
|
||||
pill = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = width)
|
||||
|> tangentialArc(end = [0, height])
|
||||
|> xLine(length = -width)
|
||||
|> tangentialArc(endAbsolute = profileStart())
|
||||
|> close()
|
||||
```
|
||||
It should look like this:
|
||||
|
||||

|
||||
|
||||
Now we're going to extrude it up into the third axis, making a 3D solid.
|
||||
|
||||
```kcl
|
||||
height = 4
|
||||
width = 8
|
||||
|
||||
// Add this line!
|
||||
depth = 10
|
||||
|
||||
pill = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = width)
|
||||
|> tangentialArc(end = [0, height])
|
||||
|> xLine(length = -width)
|
||||
|> tangentialArc(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|
||||
// Add this line!
|
||||
// This line transforms the 2D sketch into a 3D solid.
|
||||
|> extrude(length = depth)
|
||||
```
|
||||
|
||||
You should see something like this:
|
||||
|
||||

|
||||
|
||||
The [`extrude`] function takes a distance, which is how far along the third axis to extrude. Every plane has a _normal_, or an axis which is _tangent_ to the plane. For the plane XZ, this is the Y axis. This normal, or tangent, or axis perpendicular to the plane, is the direction that extrudes go along.
|
||||
|
||||
## Sweep
|
||||
|
||||
An extrude takes some 2D sketch and drags it up in a straight line along the normal axis. A _sweep_ is like an extrude, but the shape isn't just moved along a straight line: it could be moved along any path. Let's reuse our previous pill-shape example, but this time we'll sweep it instead of extruding it. First, we have to define a path that the sweep will take. Let's add one:
|
||||
|
||||
```kcl=path_for_sweep
|
||||
height = 4
|
||||
width = 8
|
||||
depth = 10
|
||||
pill = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = width)
|
||||
|> tangentialArc(end = [0, height])
|
||||
|> xLine(length = -width)
|
||||
|> tangentialArc(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
```
|
||||
|
||||

|
||||
|
||||
Now we'll add the [`sweep`] call, like `swept = sweep(pill, path = sweepPath)`, which will drag our 2D pill sketch along the path we defined.
|
||||
|
||||
```kcl=swept_along_path
|
||||
height = 4
|
||||
width = 8
|
||||
depth = 10
|
||||
pill = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = width)
|
||||
|> tangentialArc(end = [0, height])
|
||||
|> xLine(length = -width)
|
||||
|> tangentialArc(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Sweep the pill along the path
|
||||
swept = sweep(pill, path = sweepPath)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`sweep`] call has several other options you can set, so read its docs page for more information.
|
||||
|
||||
## Revolve
|
||||
|
||||
Revolves are the other common way to make a 3D shape. Let's start with a 2D shape, like a basic circle.
|
||||
|
||||
|
||||
```kcl=circle
|
||||
startSketchOn(XZ)
|
||||
|> circle(center = [-200, 0], radius = 100)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`revolve`] function takes a shape and revolves it, dragging it around an axis. Let's revolve it around the Y axis (which is perpendicular to XZ, the plane we're sketching on), to make a donut shape.
|
||||
|
||||
```kcl=donut
|
||||
startSketchOn(XZ)
|
||||
|> circle(center = [-200, 0], radius = 100)
|
||||
|> revolve(axis = Y)
|
||||
```
|
||||
|
||||

|
||||
|
||||
There's an optional argument called `angle`. In the above example, we didn't provide it, so it defaulted to 360 degrees. But we can set it to 240 degrees, and get two thirds of a donut:
|
||||
|
||||
```kcl=donut240
|
||||
startSketchOn(XZ)
|
||||
|> circle(center = [-200, 0], radius = 100)
|
||||
|> revolve(axis = Y, angle = 240)
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Spheres
|
||||
|
||||
You can make a sphere by revolving a semicircle its full 360 degrees. First, let's make a semicircle:
|
||||
|
||||
```kcl=semicircle
|
||||
radius = 10
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> yLine(length = radius * 2)
|
||||
|> arc(angleStart = 90, angleEnd = 270, radius = radius)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Then we can `close()` it and add a call to `revolve(axis = Y, angle = 360)` to revolve it into a sphere:
|
||||
|
||||
```kcl=sphere
|
||||
radius = 10
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> yLine(length = radius * 2)
|
||||
|> arc(angleStart = 90, angleEnd = 270, radius = radius)
|
||||
|> close()
|
||||
|> revolve(axis = Y, angle = 360)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Lofts
|
||||
|
||||
All previous methods -- extrudes, sweeps, revolves -- took a single 2D shape and made a single 3D solid. Lofts are a little different -- they take _multiple_ 2D shapes and join them to make a single 3D shape. A loft interpolates between various sketches, creating a volume that smoothly blends from one shape into another. Let's see an example:
|
||||
|
||||
```kcl=loft_basic
|
||||
// Sketch a square on the XY plane
|
||||
squareSketch = startSketchOn(XY)
|
||||
|> startProfile(at = [-100, 200])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, -200])
|
||||
|> line(end = [-200, 0])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|
||||
// Start a second sketch, 200 units above the XY plane.
|
||||
circleSketch = startSketchOn(offsetPlane(XY, offset = 200))
|
||||
|> circle(center = [0, 100], radius = 50)
|
||||
|
||||
// Loft the square up and into the circle.
|
||||
loft([squareSketch, circleSketch])
|
||||
```
|
||||
|
||||

|
||||
|
||||
Note that we used the [`offsetPlane`] function to start the circle sketch 200 units above the XY plane. We'll cover offsetPlane more in the chapter on [sketch on face]. The [`loft`] function has a few other advanced options you can set. One of these is `vDegree`, which affects how smoothly KCL interpolates between the shapes. Take a look at these two examples, which are identical except for vDegree. This example uses `vDegree = 1`:
|
||||
|
||||
```kcl=loft_vd1
|
||||
// Circle, 200 units below the XY plane.
|
||||
circ0 = startSketchOn(offsetPlane(XY, offset = -200))
|
||||
|> circle(center = [0, 100], radius = 50)
|
||||
|
||||
// Square on the XY plane
|
||||
squareSketch = startSketchOn(XY)
|
||||
|> startProfile(at = [-100, 200])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, -200])
|
||||
|> line(end = [-200, 0])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|
||||
// Second circle, 200 units above the XY plane.
|
||||
circ1 = startSketchOn(offsetPlane(XY, offset = 200))
|
||||
|> circle(center = [0, 100], radius = 50)
|
||||
|
||||
loftedSolid = loft([circ0, squareSketch, circ1], vDegree = 1)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The following loft is identical, but we set `vDegree = 2`. That's actually the default, so we don't need to set it, but for the sake of example we'll explicitly set it there.
|
||||
|
||||
```kcl=loft_vd2
|
||||
// Circle, 200 units below the XY plane.
|
||||
circ0 = startSketchOn(offsetPlane(XY, offset = -200))
|
||||
|> circle(center = [0, 100], radius = 50)
|
||||
|
||||
// Square on the XY plane
|
||||
squareSketch = startSketchOn(XY)
|
||||
|> startProfile(at = [-100, 200])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, -200])
|
||||
|> line(end = [-200, 0])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|
||||
// Second circle, 200 units above the XY plane.
|
||||
circ1 = startSketchOn(offsetPlane(XY, offset = 200))
|
||||
|> circle(center = [0, 100], radius = 50)
|
||||
|
||||
loftedSolid = loft([circ0, squareSketch, circ1], vDegree = 2)
|
||||
```
|
||||
|
||||

|
||||
|
||||
As you can see, the `vDegree` makes a big difference. You can view other options on the [`loft`] docs page.
|
||||
|
||||
[`extrude`]: https://zoo.dev/docs/kcl/extrude
|
||||
[`loft`]: https://zoo.dev/docs/kcl/loft
|
||||
[`offsetPlane`]: https://zoo.dev/docs/kcl-std/functions/std-offsetPlane
|
||||
[`revolve`]: https://zoo.dev/docs/kcl/revolve
|
||||
[sketch on face]: /sketch_on_face.html
|
||||
[`sweep`]: https://zoo.dev/docs/kcl/sweep
|
||||
|
233
kcl-book/src/sketch_on_face.md
Normal file
@ -0,0 +1,233 @@
|
||||
# Sketch on face
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
In the previous chapter, we looked at how KCL lets you tag edges. Tags let you query your edges (to find their length, or angle with the previous edge), or apply an edge cut (like a fillet or chamfer). But you can tag more than just edges! In this chapter, we'll learn how to tag faces, and how that lets you build more complicated 3D models.
|
||||
|
||||
## Side faces
|
||||
|
||||
Let's start with a simple example. First, we'll sketch and extrude a triangle. We'll tag its second edge as `b`.
|
||||
|
||||
```kcl=triangle_for_sketching
|
||||
length = 20
|
||||
triangle = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0])
|
||||
|> line(end = [length, length * 2], tag = $b)
|
||||
|> line(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|> extrude(length = 40)
|
||||
```
|
||||
|
||||

|
||||
|
||||
When our triangle is extruded, its 3 edges create 3 new side faces, one for each original edge. I like to imagine extrusion like an invisible hand grabbing the flat sketch and pulling it upwards into the third dimension, slowly stretching each edge until they expand to become faces. So, each new side face corresponds to an existing edge. And crucially, the faces share a tag with their parent edge. This means the face which grew out of the edge tagged `b` also has the tag `b`. We can use this to reference this face in our 3D model.
|
||||
|
||||
Now, if we want to start a new sketch _on that face_, we can do so!
|
||||
|
||||
```kcl=parse
|
||||
sketch001 = startSketchOn(triangle, face = b)
|
||||
```
|
||||
|
||||
Note that we previously passed a _plane_ (like XY or YZ) into `startSketchOn`. But now, we're passing a solid (our extruded triangle) instead. The solid has five faces (three side faces, a bottom, and a top), so we tell `startSketchOn` which face in particular we want to sketch on. The face is tagged `b` (because it was created from an edge which was tagged `b`), so we just pass that in too. Now we can start sketching on this face, and even extrude that sketch too.
|
||||
|
||||
|
||||
```kcl=triangle_with_cylinder_sketched
|
||||
length = 20
|
||||
triangle = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0])
|
||||
|> line(end = [length, length * 2], tag = $b)
|
||||
|> line(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|> extrude(length = 40)
|
||||
|
||||
cylinder = startSketchOn(triangle, face = b)
|
||||
|> circle(radius = 10, center = [0, 15])
|
||||
|> extrude(length = 40)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Great! We extruded a solid (the triangle), and could sketch on one of its faces, even extruding that sketch.
|
||||
|
||||
**Note**: When you sketch on a face, the sketch uses the _global coordinate system_. This means when you use 2D points in your sketches, they're relative to the overall global scene, and _not_ the face you're sketching on.
|
||||
|
||||
Sketching on faces is a really common pattern when designing real-world objects. A LEGO brick is a good example -- first you'd sketch the rectangular brick, then you'd sketch on its top face, adding the little bumps on top. But wait a second. How would we tell `startSketchOn` to sketch on the top face of the brick? That face isn't created from any particular edge. So we can't tag its `line` call and then reuse that tag for the face. What should we do?
|
||||
|
||||
## Standard faces
|
||||
|
||||
There's a simple solution to sketching on the top face. KCL has some built-in identifiers for the top and bottom face, [`START`] and [`END`]. We prefer the terms "start" and "end" to "top" and "bottom" because the latter depend on your camera angle, so they can be ambiguous. "Start" always refers to the original face from your 2D sketch. "End" always refers to the new face created at the end of the extrusion. Let's use them!
|
||||
|
||||
```kcl=triangle_top_and_bottom_sketches
|
||||
length = 20
|
||||
triangle = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0])
|
||||
|> line(end = [length, length * 2])
|
||||
|> line(endAbsolute = profileStart())
|
||||
|> close()
|
||||
|> extrude(length = 40)
|
||||
|
||||
cylinder = startSketchOn(triangle, face = END)
|
||||
|> circle(radius = 3, center = [0, -10])
|
||||
|> extrude(length = 40)
|
||||
|
||||
box = startSketchOn(triangle, face = START)
|
||||
|> polygon(radius = 8, numSides = 4, center = [0, -15])
|
||||
|> extrude(length = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Great! These built-in face identifiers are always available on solids. We've learned how to sketch on the top, bottom and side faces. That covers all possible faces, right? Right? Not exactly! There's one more kind of face we haven't talked about yet.
|
||||
|
||||
## Sketch on chamfer
|
||||
|
||||
When you [`chamfer`] an edge, it creates a new face, which can also be sketched on! Consider this chamfered cube from the previous chapter:
|
||||
|
||||
```kcl=chamfered_cube
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> chamfer(
|
||||
length = 2,
|
||||
tags = [
|
||||
getOppositeEdge(a),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The chamfer produced a new face, and we can sketch on it too. Firstly, we add a tag to the [`chamfer`] call, and then we can sketch on it like any other tagged face.
|
||||
|
||||
```kcl=sketch_on_chamfered_cube
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> chamfer(
|
||||
length = 2,
|
||||
tags = [
|
||||
getOppositeEdge(a),
|
||||
],
|
||||
tag = $chamferedFace
|
||||
)
|
||||
|
||||
startSketchOn(cube, face = chamferedFace)
|
||||
|> circle(radius = 1, center = [-length/2, 0])
|
||||
|> extrude(length = 40)
|
||||
```
|
||||
|
||||

|
||||
|
||||
So far, we've sketched on standard planes (like XY), on tagged faces, and on standard faces like END. There's one more place you can start sketching on: a custom plane. Let's learn how.
|
||||
|
||||
## Defining new planes
|
||||
|
||||
When you call `startSketchOn(XY)`, you're passing a _plane_ as the first argument. XY is a standard, built-in plane (remember, there are six -- XY, YZ, XZ, -XY, -YZ and -XZ). But you can easily define your own planes too! There's two ways:
|
||||
|
||||
### Offset planes
|
||||
|
||||
You can use the [`offsetPlane`] function to copy any other plane, but moved some direction up or down the third axis. For example, let's draw a small circle on XY, a medium circle on a plane 10 units above it, and a big circle 20 units above it.
|
||||
|
||||
```kcl=three_offset_planes
|
||||
r = 10
|
||||
|
||||
startSketchOn(XY)
|
||||
|> circle(center = [0, 0], radius = r)
|
||||
|
||||
startSketchOn(offsetPlane(XY, offset = 10))
|
||||
|> circle(center = [0, 0], radius = 2 * r)
|
||||
|
||||
startSketchOn(offsetPlane(XY, offset = 20))
|
||||
|> circle(center = [0, 0], radius = 3 * r)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Offset planes are a quick and easy way to create new planes by using some other plane as a template. But what if you want to create a plane that actually points in a different direction, i.e. has different axes? What if you wanted to create a plane that was pointing at an unusual angle from the global X Y and Z axes? Let's try it.
|
||||
|
||||
### Custom planes
|
||||
|
||||
You can define your own plane with your own axes like this:
|
||||
|
||||
```kcl
|
||||
customPlane = {
|
||||
origin = { x = 0, y = 1, z = 0},
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
}
|
||||
```
|
||||
|
||||
Note the custom plane has a few properties:
|
||||
|
||||
- An origin, which is a 3D point in space, using the global coordinate system (i.e. it's relative to the overall scene)
|
||||
- X and Y axes, which are defined as vectors
|
||||
|
||||
The plane's Z axis is the cross product of its X and Y axes. It's uniquely determined, so you don't need to specify it.
|
||||
|
||||
Now let's use this custom plane in a sketch. We'll build two identical cylinders, but one is on the standard XY plane, and one is on the custom plane we defined above.
|
||||
|
||||
```kcl=custom_plane
|
||||
r = 10
|
||||
startSketchOn(XY)
|
||||
|> circle(center = [100, 0], radius = r)
|
||||
|> extrude(length = 10)
|
||||
|
||||
customPlane = {
|
||||
origin = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
z = 0
|
||||
},
|
||||
xAxis = { x = 1, y = 0.5, z = 0 },
|
||||
yAxis = { x = 0, y = 0.5, z = 1 }
|
||||
}
|
||||
|
||||
startSketchOn(customPlane)
|
||||
|> circle(center = [0, 0], radius = r)
|
||||
|> extrude(length = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Great! Custom planes give you a lot of power and flexibility. You can draw sketches in any orientation now. But they can be a bit verbose and complicated to define, so you should use [`offsetPlane`] if you've already defined a plane on the same X and Y axis. You can even use `offsetPlane` to offset a custom plane, like this:
|
||||
|
||||
```kcl
|
||||
// Make a custom plane.
|
||||
customPlane = {
|
||||
origin = { x = 0, y = 1, z = 0},
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
}
|
||||
// Now offset it 20 up its normal axis.
|
||||
newPlane = offsetPlane(customPlane, offset = 20)
|
||||
```
|
||||
|
||||
Now we've learned how to sketch on all sorts of things:
|
||||
|
||||
- Standard planes like XY or -XZ
|
||||
- Tagged faces of existing solids
|
||||
- Top or bottom faces of solids, using [`START`] and [`END`]
|
||||
- Chamfered faces cut out of solids, by tagging the [`chamfer`] call
|
||||
- Custom planes (truly custom, or just offset from an existing plane)
|
||||
|
||||
This gives you a lot of flexibility in building your solids. Now it's time to learn what else we can do with these solids. The next chapter will teach you how to combine and transform them!
|
||||
|
||||
[`END`]: <https://zoo.dev/docs/kcl-std/consts/std-END>
|
||||
[`START`]: <https://zoo.dev/docs/kcl-std/consts/std-START>
|
||||
[`chamfer`]: https://zoo.dev/docs/kcl-std/functions/std-solid-chamfer
|
||||
[`offsetPlane`]: <https://zoo.dev/docs/kcl-std/functions/std-offsetPlane>
|
257
kcl-book/src/tags.md
Normal file
@ -0,0 +1,257 @@
|
||||
# Tags, Fillets and Chamfers
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Motivation: Applying a fillet
|
||||
|
||||
When you manufacture a part, you often want to smooth off its sharp edges, so they're rounded and won't accidentally cut someone who holds it.
|
||||
Let's say we're modeling a cube, like this:
|
||||
|
||||
```kcl=cube_no_fillets
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0])
|
||||
|> line(end = [0, length])
|
||||
|> line(end = [-length, 0])
|
||||
|> line(end = [0, -length])
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
```
|
||||
|
||||
It produces a cube like this:
|
||||
|
||||

|
||||
|
||||
What if we want to fillet one of its sides? Let's start simple and refer to one of the four bottom edges. Those edges were made by the four `line` calls. How can we refer back to them? Usually, to use some data in an operation, we just put that data into a variable, and pass it into a function. That won't work here, because the data is in a pipeline. So what do we do?
|
||||
|
||||
## Tagging edges
|
||||
|
||||
Simple: we _tag_ the line. A _tag_ is a reference to some data. Let's declare our first tag. We'll modify the above program by adding a tag to one of the lines, like this:
|
||||
|
||||
```kcl
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $side) // <- Add the `tag` argument here!
|
||||
|> line(end = [0, length])
|
||||
|> line(end = [-length, 0])
|
||||
|> line(end = [0, -length])
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
```
|
||||
|
||||
You declare a tag with a dollar sign, followed by its name, like `$side`. This is a new data type, called a TagDeclarator. TagDeclarators can be passed around just like any other kind of data (number, string, etc). Tagging a line is very similar to declaring a variable. Both tags and variables store data, which can be referenced later. Many KCL functions have an optional `tag` argument, including all the path-creating functions we've seen, like `line`, `tangentialArc`, `xLine`, etc.
|
||||
|
||||
Let's use this tag to make a fillet. Add the line `|> fillet(radius = 5, tags = [side])` to the end of the previous program:
|
||||
|
||||
```kcl=cube_one_fillet
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $side)
|
||||
|> line(end = [0, length])
|
||||
|> line(end = [-length, 0])
|
||||
|> line(end = [0, -length])
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(radius = 5, tags = [side])
|
||||
```
|
||||
|
||||
The [`fillet`] function accepts an argument `tags`, which expects an array of one or more tags. Note that we passed in `side`, not `$side`. The latter would be declaring a new tag, but we actually want to _reference_ an _existing_ tag. So we didn't use the `$`.
|
||||
|
||||
That program should produce a cube with one filleted edge, like this:
|
||||
|
||||

|
||||
|
||||
Nice! We could tag and fillet all four sides if we wanted to:
|
||||
|
||||
```kcl=cube_four_fillets
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(radius = 5, tags = [a, c, b, d])
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Relationships between edges
|
||||
|
||||
We've seen how to tag edges, and reference those tags later to alter edges. What about edges we don't create directly? For example, we've already filleted the four bottom edges, but how do we fillet the top four edges? We aren't creating them via `line` calls. They're created by the CAD engine in the `extrude` call. If we didn't explicitly create them with a sketch function, how do we tag them? Here's the secret --- you don't. KCL has a few helpful functions to access edges that you didn't create directly. Because we tagged the bottom edges, we can use helper functions like [`getOppositeEdge`] to reference the top edges, like this:
|
||||
|
||||
|
||||
```kcl=cube_two_opposite_fillets
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $side)
|
||||
|> line(end = [0, length])
|
||||
|> line(end = [-length, 0])
|
||||
|> line(end = [0, -length])
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(radius = 5, tags = [side, getOppositeEdge(side)])
|
||||
```
|
||||
|
||||

|
||||
|
||||
We can fillet all four top edges by tagging all four bottom edges, and then using [`getOppositeEdge`] on each:
|
||||
|
||||
```kcl=cube_eight_fillets
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(radius = 5, tags = [a, c, b, d, getOppositeEdge(a), getOppositeEdge(c), getOppositeEdge(b), getOppositeEdge(d)])
|
||||
```
|
||||
|
||||

|
||||
|
||||
So, we've filleted the bottom horizontal edges, and the top horizontal edges. What about the vertical side edges, which connect the top and bottom face? We can use [`getNextAdjacentEdge`] and [`getPreviousAdjacentEdge`] to reference them:
|
||||
|
||||
```kcl=cube_next_prev_fillets
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(
|
||||
radius = 2,
|
||||
tags = [
|
||||
a,
|
||||
getNextAdjacentEdge(a),
|
||||
getPreviousAdjacentEdge(a)
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Here, we filleted the bottom side `a` just like we did before. But we've also filleted the sides adjacent to it. We can use a similar trick to fillet all four vertical side edges:
|
||||
|
||||
|
||||
```kcl=cube_next_prev_fillets_all_sides
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> fillet(
|
||||
radius = 2,
|
||||
tags = [
|
||||
getNextAdjacentEdge(a),
|
||||
getPreviousAdjacentEdge(a),
|
||||
getNextAdjacentEdge(c),
|
||||
getPreviousAdjacentEdge(c),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Chamfers
|
||||
|
||||
A [`chamfer`] is just like a fillet, except that fillets smooth away an edge to make it round, but chamfers just make a single cut across an edge. Here's an example of the difference. Compare this chamfered cube with the filleted cubes above:
|
||||
|
||||
```kcl=chamfered_cube
|
||||
length = 20
|
||||
cube = startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [0, length], tag = $b)
|
||||
|> line(end = [-length, 0], tag = $c)
|
||||
|> line(end = [0, -length], tag = $d)
|
||||
|> close()
|
||||
|> extrude(length = length)
|
||||
|> chamfer(
|
||||
length = 2,
|
||||
tags = [
|
||||
getOppositeEdge(a),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
So we've learned to use tags to reference the lines we create, then use helper functions like [`getOppositeEdge`] to reference other geometry elsewhere in the model. But tags aren't just used for altering edges. They provide a valuable way to query and measure your models. Let's see how.
|
||||
|
||||
## Measuring with tags
|
||||
|
||||
Let's say you've got a triangle, like this:
|
||||
|
||||
```kcl
|
||||
length = 20
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0])
|
||||
|> line(end = [length, length * 2])
|
||||
|> line(endAbsolute = profileStart())
|
||||
```
|
||||
|
||||
Let's ask a simple question. How long is each side of the triangle?
|
||||
|
||||
It sounds simple, but to actually calculate it, you'd have to break out a pencil and paper, then do some trigonometry. The problem is, the length doesn't appear anywhere in the `line` call. The lines are defined by their start and end points, and the length is an implicit property of those. Defining lines as a start and end is helpful, but it means important properties, like length, can't be read from our source code.
|
||||
|
||||
However, tags give us a simple way to refer to each line, and then query them for properties like length with the [`segLen`] function. Let's update our program:
|
||||
|
||||
```kcl
|
||||
length = 20
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [-length, -length])
|
||||
|> line(end = [length, 0], tag = $a)
|
||||
|> line(end = [length, length * 2], tag = $b)
|
||||
|> line(endAbsolute = profileStart(), tag = $c)
|
||||
|
||||
lenA = segLen(a)
|
||||
lenB = segLen(b)
|
||||
lenC = segLen(c)
|
||||
```
|
||||
|
||||
Now you can open up the Variables pane and look at the `lenA`, `lenB` and `lenC` variables to find each side's length. That's pretty useful! And if you want to use those lengths elsewhere in your code, you can! You could start drawing lines where the end is `[lenA, 0]` for example, or plug those lengths into other calculations.
|
||||
|
||||
KCL has several other helper functions, like [`segAng`], which helps you find the angle between two lines. Let's measure the angles in a right-angle triangle:
|
||||
|
||||
```kcl
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = 20)
|
||||
|> yLine(length = 10, tag = $b)
|
||||
|> line(endAbsolute = profileStart(), tag = $c)
|
||||
|
||||
angleB = segAng(b)
|
||||
angleC = segAng(c)
|
||||
```
|
||||
|
||||
You can open up the Variables panel and view the relevant angles! There are other helpers too, like [`segStart`] and [`segEnd`] to find a line's start and end, respectively. Take a look at the KCL standard library docs to find them all.
|
||||
|
||||
|
||||
KCL's tagging system is simple, but powerful. It lets you build up a model (like a cube) from a simple flat shape (your square) and a transformation (like extrusion). Although the transformations create a lot of geometry (for instance, this single extrude call creates 8 edges and five faces), you don't need verbose, complicated labels for all of these features. Instead, you can tag the geometry you've explicitly created, and use simple functions like [`getOppositeEdge`] to reference related geometry. This is much easier than trying to label every edge and face in a model. In the next chapter, we'll explore more interesting uses of tags, like starting new sketches from existing 3D models.
|
||||
|
||||
[`chamfer`]: https://zoo.dev/docs/kcl-std/functions/std-solid-chamfer
|
||||
[`fillet`]: https://zoo.dev/docs/kcl-std/functions/std-solid-fillet
|
||||
[`getNextAdjacentEdge`]: https://zoo.dev/docs/kcl-std/getNextAdjacentEdge
|
||||
[`getOppositeEdge`]: https://zoo.dev/docs/kcl-std/getOppositeEdge
|
||||
[`getPreviousAdjacentEdge`]: https://zoo.dev/docs/kcl-std/getPreviousAdjacentEdge
|
||||
[`segAng`]: https://zoo.dev/docs/kcl-std/segAng
|
||||
[`segEnd`]: https://zoo.dev/docs/kcl-std/segEnd
|
||||
[`segLen`]: https://zoo.dev/docs/kcl-std/segLen
|
||||
[`segStart`]: https://zoo.dev/docs/kcl-std/segStart
|
207
kcl-book/src/transform_3d.md
Normal file
@ -0,0 +1,207 @@
|
||||
# Transforming 3D solids
|
||||
|
||||
We've covered many different ways to create 3D solids from 2D sketches, but what can we do with our solids afterwards? In this chapter we'll cover how to combine them via union, intersection and subtraction. This is sometimes called _constructive solid geometry_. We'll also look at how to scale, rotate or translate them. But before we get to that, let's start with something a little fun:
|
||||
|
||||
## Colour
|
||||
|
||||
So far, all our models have used the standard shiny grey metal appearance. But you can customize this! Let's change the texture. We'll make three cubes: one with the normal color, one green, and one a shiny metallic green.
|
||||
|
||||
```kcl=cube_textures
|
||||
offset = 25
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|
||||
greenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
// The appearance call lets you set a color using hexadecimal notation.
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCubeShiny = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset * 2])
|
||||
|> extrude(length = 10)
|
||||
// You can also set the metalness and roughness, as percentages between 0 and 100.
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`appearance`] call takes in three arguments, each of which is optional. You can provide:
|
||||
|
||||
- A `color` as a hexadecimal number like `#0044ff`. The first two digits represent red, the next two green, and the last two blue. You can use an [online color picker] to play with the format.
|
||||
- A `metalness` percentage, which is a number between 0 and 100.
|
||||
- A `roughness` percentage, which is a number between 0 and 100.
|
||||
|
||||
This is helpful for making your different solids stand out from each other. We'll be using the `appearance` call in our examples to help make it clear which KCL snippets correspond to which objects in the rendered images.
|
||||
|
||||
## Translation
|
||||
|
||||
We can transform solids, keeping them _basically_ the same -- the same number of sides, edges, and faces -- but changing some of their other properties.
|
||||
|
||||
Firstly, we can [`translate`] them (shifting them around in their coordinate system), like this:
|
||||
|
||||
```kcl=translate_cubes
|
||||
offset = 25
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|
||||
brightGreenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
// Shift the shape's position along X, Y and Z.
|
||||
|> translate(x = 4, y = -4, z = 10)
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset * 2])
|
||||
|> extrude(length = 10)
|
||||
// The translation axes are optional.
|
||||
// If you don't set X or Y, its X and Y position will remain the same.
|
||||
|> translate(z = -10)
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`translate`] call takes three arguments, `x`, `y` and `z`. Each of them is optional. If you provide one, it'll shift the solid along that axis. If you don't provide an axis, it'll remain unchanged.
|
||||
|
||||
## Scale
|
||||
|
||||
Next, we can [`scale`] them, making them bigger or smaller.
|
||||
|
||||
```kcl=scaled_cubes
|
||||
offset = 25
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|
||||
brightGreenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset])
|
||||
|> extrude(length = 10)
|
||||
// Scale all three axes, shrinking the cube
|
||||
|> scale(x = 0.5, y = 0.5, z = 0.5)
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, offset * 2])
|
||||
|> extrude(length = 10)
|
||||
// Expand the cube along one axis, shrink it across another, and leave
|
||||
// the third axis unchanged.
|
||||
|> scale(z = 0.25, y = 2)
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
The [`scale`] call works similarly. You provide one or more axes -- if you don't provide an axis, it's left unchanged. Numbers less than 1 will shrink the solid (e.g. 0.25 means 1/4th its original size). Numbers larger than 1 will expand the solid (e.g. 4 means 4 times its original size).
|
||||
|
||||
## Rotation
|
||||
|
||||
Lastly, we can rotate them. The [`rotate`] call is similar to translate and rotate: it takes a number of properties -- different ways to rotate -- all of which are optional, and if you don't provide one, it stays unchanged. These properties are roll, pitch and yaw.
|
||||
|
||||
**Roll**: Imagine spinning a pencil on its tip - that's a roll movement.
|
||||
**Pitch**: Think of a seesaw motion, where the object tilts up or down along its side axis.
|
||||
**Yaw**: Like turning your head left or right, this is a rotation around the vertical axis
|
||||
|
||||
Let's see an example:
|
||||
|
||||
```kcl=rotated_cubes
|
||||
offset = 25
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|
||||
brightGreenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> translate(z = offset)
|
||||
|> rotate(roll= 45)
|
||||
|> appearance(color = "#00ff00")
|
||||
|
||||
greenCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> translate(z = 2 * offset)
|
||||
|> rotate(pitch = 45)
|
||||
|> appearance(color = "#00ff00", metalness = 90, roughness = 10)
|
||||
|
||||
blueCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> translate(z = 3 * offset)
|
||||
|> rotate(yaw = 45)
|
||||
|> appearance(color = "#0000ff", metalness = 90, roughness = 10)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Note that these rotations are all around their own center (not the center of the plane).
|
||||
|
||||
Roll, pitch and yaw are one valid way to represent a rotation, but there are other ways too. You could also choose an axis, and rotate around that axis. For example, let's put 4 cubes at the same point, and then rotate them each a little bit around the axis.
|
||||
|
||||
```kcl=rotated_cubes_axis
|
||||
angle = 15
|
||||
|
||||
greyCube = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> appearance(color = "#33ff00")
|
||||
|
||||
green1 = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> rotate(axis = [1, 0, 0], angle = angle)
|
||||
|> appearance(color = "#337700")
|
||||
|
||||
green2 = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> rotate(axis = [1, 0, 0], angle = angle * 2)
|
||||
|> appearance(color = "#334400")
|
||||
|
||||
green3 = startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> polygon(radius = 10, numSides = 4, center = [0, 0])
|
||||
|> extrude(length = 10)
|
||||
|> rotate(axis = [1, 0, 0], angle = angle * 3)
|
||||
|> appearance(color = "#332200")
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Using transformations
|
||||
|
||||
You can combine multiple transformations, for example a translate and scale: `|> translate(x = 10) |> scale (y = 20)`. This can really simplify your mechanical engineering. For example, if you need to produce two cubes, rotated at different angles, which of these approaches sounds easier?
|
||||
|
||||
1. Make one cube using `polygon` with 4 sides, and then design the other cube from scratch using `line` calls that join the 4 rotated points
|
||||
2. Make one cube using `polygon`, and then make a second cube by copying the first cube and adding a `rotation`call
|
||||
|
||||
These transformations make your job easier by letting you reuse work from previous designs. Once you know how to sketch a cube, you don't need to recalculate your cube every time it needs to grow, rotate or get moved over. You can just use our simple transformation functions. Recalculating a cube each time is annoying, but possible. For more complicated geometry, with weird curves and many edges, redoing all your calculations to handle different scales and rotations can be _very_ difficult and waste a lot of time! So don't recalculate them. Just reuse your work and transform it.
|
||||
|
||||
[`appearance`]: https://zoo.dev/docs/kcl-std/appearance
|
||||
[`translate`]: https://zoo.dev/docs/kcl-std/translate
|
||||
[`scale`]: https://zoo.dev/docs/kcl-std/scale
|
||||
[`rotate`]: https://zoo.dev/docs/kcl-std/rotate
|
||||
[online color picker]: https://g.co/kgs/wVN95r4
|
40
kcl-book/src/units_of_measurement.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Units of measurement
|
||||
|
||||
KCL tracks the units that each distance uses. This can help you accurately translate your engineering requirements or formula into KCL, without pulling out a calculator to convert between inches and centimeters.
|
||||
|
||||
For example, you can put a unit like `20cm` or `20in` as the length of a line. Here's three different lines of length 20 centimeters, inches and millimeters.
|
||||
|
||||
```kcl=lines_units
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, -100])
|
||||
|> xLine(length = 20mm)
|
||||
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = 20cm)
|
||||
|
||||
startSketchOn(XY)
|
||||
|> startProfile(at = [0, 100])
|
||||
|> xLine(length = 20in)
|
||||
```
|
||||
|
||||

|
||||
|
||||
Other suffixes include metres (`m`), feet (`ft`) and yards (`yd`).
|
||||
|
||||
You can also set the units for angle measurements. Here's two toruses, one of which revolves 6 degrees (very little) and the other, 6 radians (almost a full revolution).
|
||||
|
||||
|
||||
```kcl=donut_angle_units
|
||||
// Revolve 6 degrees
|
||||
startSketchOn(XZ)
|
||||
|> circle(center = [-200, -200], radius = 50)
|
||||
|> revolve(axis = Y, angle = 6deg)
|
||||
|
||||
// Revolve 6 radians
|
||||
startSketchOn(XZ)
|
||||
|> circle(center = [200, 200], radius = 50)
|
||||
|> revolve(axis = Y, angle = 6rad)
|
||||
```
|
||||
|
||||

|
99
kcl-book/src/variables.md
Normal file
@ -0,0 +1,99 @@
|
||||
## Variables
|
||||
<!-- toc -->
|
||||
|
||||
Let's get comfortable with basic KCL first, before we start designing parts. Don't worry, we'll get to real mechanical engineering very soon. For now, let's start with some math.
|
||||
|
||||
Here's a simple KCL program. Open up the Zoo Design Studio, make a new project, then open the KCL code panel (on the left). Enter this text in:
|
||||
|
||||
```kcl
|
||||
width = 1
|
||||
height = 2
|
||||
area = width * height
|
||||
```
|
||||
|
||||
This simple program declares three _variables_. Variables are little bits of data you can define or calculate. Here we define `width` and `height` and assign their value immediately. We write their value right in there, as `width = 1` and `height = 2`. You can see that a variable declaration has the variable's _name_ (e.g. `width`), then an equals sign (=) and then its value (e.g. 1).
|
||||
|
||||
The `area` variable is very similarly, except instead of defining the exact number, we define it as a calculation. We define it as `width * height`. We could have defined `area` as just `area = 2`, but this way, anyone else who reads the code can understand _why_ the area is 2 -- it's because we're calculating some rectangular area with a width and height.
|
||||
|
||||
In this simple case, we can calculate the area in our head. It's going to be 2. But what if you're calculating something more complex? Well, take a look at the Variables panel (on the left).
|
||||
|
||||

|
||||
|
||||
This panel shows every variable and its value. You can look up the value of `area` here. It's 2, just like we expected. For this simple example it's not necessary to look it up, but for more complicated cases it can be very helpful! This way, you can do all your engineering calculations in KCL. You can treat it like a really advanced calculator, where big equations can be broken into smaller named variables, and their value can be inspected independently.
|
||||
|
||||
Note that once you declare a variable, you cannot redeclare it, or change its value.
|
||||
|
||||
## Basic data types
|
||||
|
||||
All the variables in the previous section stored numbers. But KCL can store other types of data too. Let's see some examples. These aren't all the types of data KCL can store, but it's a good starting point. We'll learn more data types later in this book as we get into more specialized features for designing parts.
|
||||
|
||||
### Number
|
||||
|
||||
You just saw how basic numbers work in the example above. Numbers can also be fractional or negative.
|
||||
|
||||
#### Examples
|
||||
|
||||
- `width = 1`
|
||||
- `diameter = 1.5`
|
||||
- `offset = -2.3`
|
||||
|
||||
### Booleans
|
||||
|
||||
A boolean value is either true, or false. That's it! Just those two choices. Booleans are useful for changing details of KCL functions, like changing whether a semicircle is drawing clockwise or not.
|
||||
|
||||
#### Examples
|
||||
|
||||
- `clockwise = false`
|
||||
- `isConstructionGeometry = true`
|
||||
|
||||
### String
|
||||
|
||||
A _string_ stores text. "String" is the software-engineering term for text. We probably should have called this just "Text" in KCL, but oh well. You can think of it as "stringing" several letters together to make words. They're not currently used very often in KCL, except to set colours (with the hexadecimal colour codes you might see in Photoshop, Figma or Canva). In the future, you'll be able to use strings to embed text into your models (e.g. for engraving text into your objects).
|
||||
|
||||
#### Examples
|
||||
|
||||
- `textToEngrave = "My Phone"`
|
||||
- `red = "#FF0000"`
|
||||
|
||||
## Collection types
|
||||
|
||||
All the previous data types stored basically one piece of data. It might be a number, or text, or a true/false value, but it's basically a single piece of data. KCL variables can also store multiple pieces of data, kept together under a single variable name. Let's see some examples.
|
||||
|
||||
### Arrays
|
||||
|
||||
An array is a list of data, like the four numbers `[1, 2, 3, 4]` or these three colours `["#ff0000", "#cccc00", "#44ff00"]`. These arrays contain other data. We say that arrays contain _items_. The two previous example arrays had 4 items and 3 items respectively. Sometimes the items of an array are called their _elements_. The terms "elements" and "items" are synonyms, you can use them interchangeably.
|
||||
|
||||
To access the items in an array, you use square brackets and the number item you want. For example, `myArray[0]` will get the first item from the array, `myArray[1]` will get the second, and so on. Yes, that's right, the first item is item 0, not item 1! This might be strange for new programmers, but it's how almost every programming language works, so we felt it was important to stick with that convention, so that your KCL code works like similar code in Python, JavaScript, C or other languages.
|
||||
|
||||
If you try to access an item beyond what the array contains -- for example, the fifth element of `[1, 2]` -- you'll get an error and the KCL program will stop.
|
||||
|
||||
Arrays can also be defined as a _range_ of values, for example, `[1..5]` is a shorthand for the array `[1, 2, 3, 4, 5]`. Note that the range is _inclusive_ at both ends (it includes both the start and end of the range in the array).
|
||||
|
||||
#### Examples
|
||||
|
||||
- `colors = ["#ff0000", "#cccc00", "#44ff00"]`
|
||||
- `red = colors[0]`
|
||||
- `sizes = [33.5, 31.5, 30]`
|
||||
- `smallest = sizes[2]`
|
||||
- `arrayOfArrays = [[1, 2, 3], [1, 4, 6]]`
|
||||
- `firstFiveNumbers = [1..5]`
|
||||
|
||||
### Points
|
||||
|
||||
To properly dimension and sketch out your designs, you'll frequently need to select specific points on a plane. In KCL, points can be stored in variables and used just like any other data type. We actually store points as arrays. An array with 2 elements Arrays are really important in KCL, because we use them to represent 2D points on a plane (e.g. the origin `[0, 0]`) or 3D points in space.
|
||||
|
||||
#### Examples
|
||||
- `origin = [0, 0]`
|
||||
- `myPoint = [4, 0, 0]`
|
||||
- `myPointX = myPoint[0]`
|
||||
- `myPointY = myPoint[1]`
|
||||
- `myPointZ = myPoint[2]`
|
||||
|
||||
### Objects
|
||||
|
||||
Sometimes, you need to store several pieces of related data together. KCL has _objects_ which contain several _fields_. Fields have a key, which is always text (a string), and a value, which can be any kind of KCL value. Even another object!
|
||||
|
||||
#### Examples
|
||||
- `sphere = { radius = 4, center = [0, 0, 3.2] }`
|
||||
- `wires = { positive = [1, 2], negative = [3, 4], resistance = 0.3 }`
|
||||
- `components = { name = "Flange", holes = { inner = [[0, 0], [1, 0]], outer = [[4, 4]] } }`
|
14
rust/Cargo.lock
generated
@ -387,6 +387,12 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
@ -1813,6 +1819,14 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-book-tester"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-bumper"
|
||||
version = "0.1.69"
|
||||
|
@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"kcl-book-tester",
|
||||
"kcl-bumper",
|
||||
"kcl-derive-docs",
|
||||
"kcl-directory-test-macro",
|
||||
|
1
rust/kcl-book-tester/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
23
rust/kcl-book-tester/Cargo.lock
generated
Normal file
@ -0,0 +1,23 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
|
||||
[[package]]
|
||||
name = "kcl-book-tester"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
]
|
8
rust/kcl-book-tester/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "kcl-book-tester"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
camino = "1.1.9"
|
114
rust/kcl-book-tester/bacon.toml
Normal file
@ -0,0 +1,114 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
#
|
||||
# You may check the current default at
|
||||
# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml
|
||||
|
||||
default_job = "check"
|
||||
env.CARGO_TERM_COLOR = "always"
|
||||
|
||||
[jobs.run]
|
||||
command = ["./run"]
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets"]
|
||||
need_stdout = false
|
||||
|
||||
# Run clippy on the default target
|
||||
[jobs.clippy]
|
||||
command = ["cargo", "clippy"]
|
||||
need_stdout = false
|
||||
|
||||
# Run clippy on all targets
|
||||
# To disable some lints, you may change the job this way:
|
||||
# [jobs.clippy-all]
|
||||
# command = [
|
||||
# "cargo", "clippy",
|
||||
# "--all-targets",
|
||||
# "--",
|
||||
# "-A", "clippy::bool_to_int_with_if",
|
||||
# "-A", "clippy::collapsible_if",
|
||||
# "-A", "clippy::derive_partial_eq_without_eq",
|
||||
# ]
|
||||
# need_stdout = false
|
||||
[jobs.clippy-all]
|
||||
command = ["cargo", "clippy", "--all-targets"]
|
||||
need_stdout = false
|
||||
|
||||
# This job lets you run
|
||||
# - all tests: bacon test
|
||||
# - a specific test: bacon test -- config::test_default_files
|
||||
# - the tests of a package: bacon test -- -- -p config
|
||||
[jobs.test]
|
||||
command = ["cargo", "test"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.nextest]
|
||||
command = [
|
||||
"cargo", "nextest", "run",
|
||||
"--hide-progress-bar", "--failure-output", "final"
|
||||
]
|
||||
need_stdout = true
|
||||
analyzer = "nextest"
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--no-deps"]
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||
need_stdout = false
|
||||
on_success = "back" # so that we don't open the browser at each change
|
||||
|
||||
# # You can run your application and have the result displayed in bacon,
|
||||
# # if it makes sense for this crate.
|
||||
# [jobs.run]
|
||||
# command = [
|
||||
# "cargo", "run",
|
||||
# # put launch parameters for your program behind a `--` separator
|
||||
# ]
|
||||
# need_stdout = true
|
||||
# allow_warnings = true
|
||||
# background = true
|
||||
|
||||
# Run your long-running application (eg server) and have the result displayed in bacon.
|
||||
# For programs that never stop (eg a server), `background` is set to false
|
||||
# to have the cargo run output immediately displayed instead of waiting for
|
||||
# program's end.
|
||||
# 'on_change_strategy' is set to `kill_then_restart` to have your program restart
|
||||
# on every change (an alternative would be to use the 'F5' key manually in bacon).
|
||||
# If you often use this job, it makes sense to override the 'r' key by adding
|
||||
# a binding `r = job:run-long` at the end of this file .
|
||||
[jobs.run-long]
|
||||
command = [
|
||||
"cargo", "run",
|
||||
# put launch parameters for your program behind a `--` separator
|
||||
]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
background = false
|
||||
on_change_strategy = "kill_then_restart"
|
||||
|
||||
# This parameterized job runs the example of your choice, as soon
|
||||
# as the code compiles.
|
||||
# Call it as
|
||||
# bacon ex -- my-example
|
||||
[jobs.ex]
|
||||
command = ["cargo", "run", "--example"]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
|
1
rust/kcl-book-tester/run
Executable file
@ -0,0 +1 @@
|
||||
cargo run -q -- /Users/adamchalmers/kc-repos/modeling-app new
|
162
rust/kcl-book-tester/src/main.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use camino::Utf8Path;
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
println!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
All,
|
||||
New,
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut envs = std::env::args();
|
||||
envs.next();
|
||||
let Some(modeling_app_dir) = envs.next() else {
|
||||
anyhow::bail!("Must provide the modeling-app directory as the first arg");
|
||||
};
|
||||
let mode = envs.next().unwrap_or_default();
|
||||
let mode = match mode.as_str() {
|
||||
"all" => Mode::All,
|
||||
"new" => Mode::New,
|
||||
other => anyhow::bail!("Unknown mode '{other}', must be either 'all' or 'new')"),
|
||||
};
|
||||
let modeling_app_dir = Utf8PathBuf::from(modeling_app_dir);
|
||||
let book_dir = kcl_book_dir(modeling_app_dir);
|
||||
let files = read_markdown_files(&book_dir)?;
|
||||
let kcl_programs = files.flat_map(kcl_code_blocks);
|
||||
|
||||
for code_block in kcl_programs {
|
||||
println!(
|
||||
"Running {} {} {}",
|
||||
code_block.file,
|
||||
code_block.block_number,
|
||||
code_block.name.clone().unwrap_or_else(|| "unnamed".to_owned())
|
||||
);
|
||||
if let Some(name) = code_block.name {
|
||||
render_snapshot(code_block.contents, &name, book_dir.clone(), mode)?;
|
||||
} else {
|
||||
render_snapshot(code_block.contents, "foo", book_dir.clone(), mode)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the KCL program via the `zoo` CLI, and writes the output PNG to the right
|
||||
/// location within the book's images dir.
|
||||
fn render_snapshot(program: String, name: &str, book_dir: Utf8PathBuf, mode: Mode) -> Result<()> {
|
||||
let mut png_dst = book_dir.clone();
|
||||
png_dst.push("images");
|
||||
png_dst.push("dynamic");
|
||||
png_dst.push(name);
|
||||
png_dst.set_extension("png");
|
||||
if mode == Mode::New && std::fs::exists(&png_dst)? {
|
||||
// PNG already exists, so skip it.
|
||||
return Ok(());
|
||||
}
|
||||
let mut cmd = Command::new("zoo")
|
||||
.args(["kcl", "snapshot", "-", png_dst.as_ref()])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("could not spawn 'zoo' cli");
|
||||
let mut cmd_stdin = cmd.stdin.take().expect("Could not open stdin");
|
||||
std::thread::spawn(move || {
|
||||
cmd_stdin
|
||||
.write_all(program.as_bytes())
|
||||
.expect("Failed to write to stdin");
|
||||
});
|
||||
let cmd_out = cmd.wait_with_output()?;
|
||||
if !cmd_out.stderr.is_empty() {
|
||||
println!("Stderr from {name}.kcl: {}", String::from_utf8_lossy(&cmd_out.stderr));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kcl_book_dir(mut modeling_app_dir: Utf8PathBuf) -> Utf8PathBuf {
|
||||
modeling_app_dir.push("kcl-book");
|
||||
modeling_app_dir.push("src");
|
||||
modeling_app_dir
|
||||
}
|
||||
|
||||
fn read_markdown_files(book_dir: &Utf8Path) -> Result<impl Iterator<Item = Utf8PathBuf>> {
|
||||
let files = book_dir
|
||||
.read_dir_utf8()
|
||||
.context(format!("could not read dir {book_dir}"))?;
|
||||
|
||||
let it = files
|
||||
.filter_map(|file| file.ok())
|
||||
.map(|file| file.path().to_path_buf())
|
||||
.filter(|file| {
|
||||
let Some(ext) = file.extension() else {
|
||||
return false;
|
||||
};
|
||||
ext == "md"
|
||||
});
|
||||
|
||||
Ok(it)
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct CodeBlock {
|
||||
contents: String,
|
||||
name: Option<String>,
|
||||
file: String,
|
||||
block_number: usize,
|
||||
}
|
||||
|
||||
impl CodeBlock {
|
||||
fn new(file: String, block_number: usize) -> Self {
|
||||
Self {
|
||||
contents: String::new(),
|
||||
name: None,
|
||||
file,
|
||||
block_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract all KCL code blocks from a Markdown file.
|
||||
fn kcl_code_blocks(p: Utf8PathBuf) -> impl Iterator<Item = CodeBlock> {
|
||||
let file_contents = match std::fs::read_to_string(&p) {
|
||||
Ok(contents) => contents,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading {p}: {e}");
|
||||
return Vec::new().into_iter();
|
||||
}
|
||||
};
|
||||
let mut blocks: Vec<CodeBlock> = Vec::new();
|
||||
let mut in_block = false;
|
||||
let mut curr_block = CodeBlock::new(p.to_string(), 0);
|
||||
for line in file_contents.lines() {
|
||||
if line.trim_start().starts_with("```kcl") {
|
||||
in_block = true;
|
||||
curr_block.name = line.strip_prefix("```kcl=").map(|s| s.to_owned());
|
||||
continue;
|
||||
}
|
||||
if line.trim() == "```" {
|
||||
in_block = false;
|
||||
blocks.push(curr_block.clone());
|
||||
curr_block = CodeBlock::new(p.to_string(), blocks.len());
|
||||
continue;
|
||||
}
|
||||
if !in_block {
|
||||
continue;
|
||||
}
|
||||
curr_block.contents.push_str(line);
|
||||
curr_block.contents.push('\n');
|
||||
}
|
||||
blocks.into_iter()
|
||||
}
|