Compare commits

...

11 Commits

Author SHA1 Message Date
5bc6e76d7b Start UoM chapter 2025-05-12 13:43:06 -05:00
556396acf3 Lofts 2025-05-12 12:32:25 -05:00
56c25cd1f7 Document sweeps 2025-05-12 12:32:25 -05:00
3cd9454268 Show users how to make spheres 2025-05-12 12:32:25 -05:00
55d27ee391 Fix typo in tags.md around images 2025-05-12 12:32:25 -05:00
f35485751d Document range notation 2025-05-12 12:32:24 -05:00
2b7e924eb9 typos
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-12 12:32:24 -05:00
63f2756182 Fix typos 2025-05-12 12:32:24 -05:00
1a1c2c038b Fix typos 2025-05-12 12:32:24 -05:00
7e2a75509b Fix typo
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-12 12:32:24 -05:00
4b7537356f First draft of KCL book chapters from Adam 2025-05-12 12:32:23 -05:00
99 changed files with 2756 additions and 4 deletions

View File

@ -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
View File

@ -86,3 +86,8 @@ venv
.vscode-test .vscode-test
.biome/ .biome/
.million .million
# KCL book
kcl-book/book
kcl-book/src/images/dynamic/foo.png

1
kcl-book/TODO.md Normal file
View File

@ -0,0 +1 @@

15
kcl-book/book.toml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

31
kcl-book/src/SUMMARY.md Normal file
View 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)

View 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
View 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].
![Union, intersection and complement on 2D circles](images/static/boolean_2d_ops.png)
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")
```
![Two cubes, one green, one blue](images/dynamic/two_cubes.png)
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])
```
![Two cubes, one green, one blue](images/dynamic/two_cubes_union.png)
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])
```
![Intersection of the two cubes](images/dynamic/two_cubes_intersection.png)
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])
```
![Green cube with blue cube subtracted](images/dynamic/two_cubes_subtraction.png)
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

View 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)
```
![The pipe flange](images/dynamic/specific_flange.png)
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,
)
```
![A different instantiation of the parametric flange](images/static/new_flange.png)
And let's try one more:
```kcl
flange(
numHoles = 20,
holeRadius = 3,
radius = 90,
thickness = 20,
holeEdgeGap = 15,
)
```
![Another pipe flange](images/static/another_flange.png)
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)
```
![Three cubes with different textures](images/dynamic/cube_textures.png)
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)
```
![Three flat cubes with different textures](images/dynamic/three_short_cubes.png)
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

29
kcl-book/src/intro.md Normal file
View 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
View 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)
```
![Three mapped cubes](images/dynamic/three_map_cubes.png)
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)
```
![Three mapped cubes](images/dynamic/three_map_cubes_color.png)
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
View File

@ -0,0 +1 @@
# Modules

258
kcl-book/src/patterns.md Normal file
View 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])
```
![Using linear patterns to replicate a cylinder](images/dynamic/linear_pattern.png)
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,
)
```
![Using circular patterns to replicate a cube](images/dynamic/circular_cubes_false.png)
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,
)
```
![Using circular patterns to replicate a cube](images/dynamic/circular_cubes_true.png)
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,
)
```
![Using semi-circular patterns to replicate a cube](images/dynamic/circular_cubes_partway.png)
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,
)
```
![Using a pattern to repeatedly sketch on face](images/dynamic/pattern_sof.png)
## 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:
![Grid of tiles, using a pattern transform](images/dynamic/xform_grid.png)
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!
![chessboard of tiles, using a pattern transform](images/dynamic/xform_chessboard.png)
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.
![chessboard of tiles, using a pattern transform](images/dynamic/cube_spiral.png)
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,
)
```
![Patterning 2D sketches of circles](images/dynamic/pattern2d.png)
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)
```
![Subtracting a pattern of circles, then extruding](images/dynamic/subtract2d_patterns.png)
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
View 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.

View 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.
![A square, made by reducing an array](images/dynamic/reduce_square.png)
## 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)
```
![A 7-sided polygon, made by reducing an array](images/dynamic/reduce_polygon.png)
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.
![A comb with a variable number of teeth](images/dynamic/reduce_comb.png)
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
View 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:
![Result of running program 1](images/static/triangle_closed.png)
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:
![Result of running program 1](images/static/triangle_open.png)
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

View 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:
![A pill-shape made from xLines and tangentialArcs](images/static/pill_sketch.png)
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:
![A spiral made from many tangential arcs](images/static/spiral.png)
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)
```
![A simple circle](images/dynamic/basic_circle.png)
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
View 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:
![2D pill, before extruding](images/static/pill_2d.png)
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:
![3D pill, after extruding](images/static/pill_3d.png)
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])
```
![A 2D pill shape, and a path we're going to sweep it along](images/dynamic/path_for_sweep.png)
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)
```
![A 2D pill shape, swept along a path to make it 3D](images/dynamic/swept_along_path.png)
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)
```
![A 2D circle, before revolving.](images/dynamic/circle.png)
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)
```
![The circle, revolved around the axis, to make a donut](images/dynamic/donut.png)
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)
```
![The circle, revolved partway around the axis, to make part of a donut](images/dynamic/donut240.png)
### 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)
```
![Sketching a semicircle](images/dynamic/semicircle.png)
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)
```
![Revolving a semicircle to make a sphere](images/dynamic/sphere.png)
## 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])
```
![Basic loft of a square into a circle](images/dynamic/loft_basic.png)
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)
```
![Loft with vDegree 1](images/dynamic/loft_vd1.png)
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)
```
![Loft with vDegree 2](images/dynamic/loft_vd2.png)
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

View 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)
```
![An extruded triangle](images/dynamic/triangle_for_sketching.png)
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)
```
![The previous triangle, with a cylinder sketched on its side face](images/dynamic/triangle_with_cylinder_sketched.png)
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)
```
![Solid with a cylinder extruded on top, and a cube extruded below](images/dynamic/triangle_top_and_bottom_sketches.png)
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),
],
)
```
![A chamfered cube](images/dynamic/chamfered_cube.png)
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)
```
![A chamfered cube, with a protruding cylinder from the chamfer](images/dynamic/sketch_on_chamfered_cube.png)
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)
```
![Circles on the XY plane, 10 above it, and 20 above it](images/dynamic/three_offset_planes.png)
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)
```
![Two cylinders, one on XY and one on a custom plane](images/dynamic/custom_plane.png)
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
View 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:
![A cube](images/dynamic/cube_no_fillets.png)
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:
![A cube with one filleted edge](images/dynamic/cube_one_fillet.png)
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])
```
![A cube with four filleted edges](images/dynamic/cube_four_fillets.png)
## 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)])
```
![A cube with one filleted edge on the bottom, and the opposite top edge filleted too](images/dynamic/cube_two_opposite_fillets.png)
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)])
```
![A cube with one filleted edge on the bottom, and the opposite top edge filleted too](images/dynamic/cube_eigth_fillets.png)
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)
],
)
```
![A cube with two side fillets and one bottom fillet](images/dynamic/cube_next_prev_fillets.png)
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),
],
)
```
![A cube with two side fillets and one bottom fillet](images/dynamic/cube_next_prev_fillets_all_sides.png)
## 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),
],
)
```
![A chamfered cube](images/dynamic/chamfered_cube.png)
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

View 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)
```
![Three cubes with different textures](images/dynamic/cube_textures.png)
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)
```
![Three translated cubes](images/dynamic/translate_cubes.png)
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)
```
![Three scaled cubes](images/dynamic/scaled_cubes.png)
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)
```
![Four rotated cubes](images/dynamic/rotated_cubes.png)
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")
```
![Four cubes, rotated around the same axis](images/dynamic/rotated_cubes_axis.png)
## 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

View 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)
```
![Three lines of length 20 mm, 20 cm and 20 inches](images/dynamic/lines_units.png)
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)
```
![Revolve of 6 degrees vs. 6 radians](images/dynamic/donut_angle_units.png)

99
kcl-book/src/variables.md Normal file
View 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).
![Result of running program 1](images/static/1.png)
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
View File

@ -387,6 +387,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.3.0" version = "0.3.0"
@ -1813,6 +1819,14 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "kcl-book-tester"
version = "0.1.0"
dependencies = [
"anyhow",
"camino",
]
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.69" version = "0.1.69"

View File

@ -1,6 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"kcl-book-tester",
"kcl-bumper", "kcl-bumper",
"kcl-derive-docs", "kcl-derive-docs",
"kcl-directory-test-macro", "kcl-directory-test-macro",

1
rust/kcl-book-tester/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

23
rust/kcl-book-tester/Cargo.lock generated Normal file
View 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",
]

View 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"

View 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
View File

@ -0,0 +1 @@
cargo run -q -- /Users/adamchalmers/kc-repos/modeling-app new

View 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()
}