Test code examples in docs and add docs for per-file settings (#5474)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-02-27 09:34:55 +13:00
committed by GitHub
parent 725c4d95f8
commit 58a9c60d0b
8 changed files with 162 additions and 42 deletions

View File

@ -6,9 +6,15 @@ layout: manual
## Table of Contents
* [Types](kcl/types)
* [Modules](kcl/modules)
* [Known Issues](kcl/KNOWN-ISSUES)
### Language
* [`Types`](kcl/types)
* [`Modules`](kcl/modules)
* [`Settings`](kcl/settings)
* [`Known Issues`](kcl/known-issues)
### Standard library
* **`std`**
* [`HALF_TURN`](kcl/const_std-HALF_TURN)
* [`QUARTER_TURN`](kcl/const_std-QUARTER_TURN)

View File

@ -20,7 +20,7 @@ export fn increment(x) {
Other files in the project can now import functions that have been exported.
This makes them available to use in another file.
```
```norun
// main.kcl
import increment from "util.kcl"
@ -48,13 +48,13 @@ export fn decrement(x) {
When importing, you can import multiple functions at once.
```
```norun
import increment, decrement from "util.kcl"
```
Imported symbols can be renamed for convenience or to avoid name collisions.
```
```norun
import increment as inc, decrement as dec from "util.kcl"
```
@ -63,13 +63,13 @@ import increment as inc, decrement as dec from "util.kcl"
`import` can also be used to import files from other CAD systems. The format of the statement is the
same as for KCL files. You can only import the whole file, not items from it. E.g.,
```
```norun
import "tests/inputs/cube.obj"
// Use `cube` just like a KCL object.
```
```
```norun
import "tests/inputs/cube-2.sldprt" as cube
// Use `cube` just like a KCL object.
@ -78,7 +78,7 @@ import "tests/inputs/cube-2.sldprt" as cube
You can make the file format explicit using a format attribute (useful if using a different
extension), e.g.,
```
```norun
@(format = obj)
import "tests/inputs/cube"
```
@ -87,7 +87,7 @@ For formats lacking unit data (such as STL, OBJ, or PLY files), the default
unit of measurement is millimeters. Alternatively you may specify the unit
by using an attirbute. Likewise, you can also specify a coordinate system. E.g.,
```
```norun
@(unitLength = ft, coords = opengl)
import "tests/inputs/cube.obj"
```

30
docs/kcl/settings.md Normal file
View File

@ -0,0 +1,30 @@
---
title: "KCL settings"
excerpt: "Documentation of settings for the KCL language and Zoo Modeling App."
layout: manual
---
# Per-file settings
Settings which affect a single file are configured using the settings attribute.
This must be at the top of the KCL file (comments before the attribute are permitted).
E.g.,
```
// The settings attribute.
@settings(defaultLengthUnit = in)
// The rest of your KCL code goes below...
x = 42 // Represents 42 inches.
```
The settings attribute may contain multiple properties separated by commas.
Valid properties are:
- `defaultLengthUnit`: the default length unit to use for numbers declared in this file.
- Accepted values: `mm`, `cm`, `m`, `in` (inches), `ft` (feet), `yd` (yards).
- `defaultAngleUnit`: the default angle unit to use for numbers declared in this file.
- Accepted values: `deg` (degrees), `rad` (radians).
These settings override any project-wide settings (configured in project.toml or via the UI).

View File

@ -74,18 +74,15 @@ fn myFn(x) {
As you can see above `myFn` just returns whatever it is given.
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
function like this:
KCL's early drafts used positional arguments, but we now use keyword arguments:
```
// If you declare a function like this
fn add(left, right) {
return left + right
}
```
You can call it like this:
```
// You can call it like this:
total = add(left = 1, right = 2)
```
@ -111,14 +108,14 @@ three = add(1, delta = 2)
It can be hard to read repeated function calls, because of all the nested brackets.
```
```norun
i = 1
x = h(g(f(i)))
```
You can make this easier to read by breaking it into many declarations, but that is a bit annoying.
```
```norun
i = 1
x0 = f(i)
x1 = g(x0)
@ -133,12 +130,12 @@ the `%` in the right-hand side.
So, this means `x |> f(%) |> g(%)` is shorthand for `g(f(x))`. The code example above, with its
somewhat-clunky `x0` and `x1` constants could be rewritten as
```
```norun
i = 1
x = i
|> f(%)
|> g(%)
|> h(%)
|> f(%)
|> g(%)
|> h(%)
```
This helps keep your code neat and avoid unnecessary declarations.
@ -147,12 +144,12 @@ This helps keep your code neat and avoid unnecessary declarations.
Say you have a long pipeline of sketch functions, like this:
```
startSketch()
|> line(%, end = [3, 4])
|> line(%, end = [10, 10])
|> line(%, end = [-13, -14])
|> close(%)
```norun
startSketchOn('XZ')
|> line(%, end = [3, 4])
|> line(%, end = [10, 10])
|> line(%, end = [-13, -14])
|> close(%)
```
In this example, each function call outputs a sketch, and it gets put into the next function call via
@ -162,12 +159,12 @@ If a function call uses an unlabeled first parameter, it will default to `%` if
means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent! So the above
could be rewritten as
```
startSketch()
|> line(end = [3, 4])
|> line(end = [10, 10])
|> line(end = [-13, -14])
|> close()
```norun
startSketchOn('XZ')
|> line(end = [3, 4])
|> line(end = [10, 10])
|> line(end = [-13, -14])
|> close()
```
Note that we are still in the process of migrating KCL's standard library to use keyword arguments. So some
@ -184,7 +181,7 @@ Tags are used to give a name (tag) to a specific path.
The syntax for declaring a tag is `$myTag` you would use it in the following
way:
```
```norun
startSketchOn('XZ')
|> startProfileAt(origin, %)
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
@ -216,7 +213,7 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
However if the code was written like this:
```
```norun
fn rect(origin) {
return startSketchOn('XZ')
|> startProfileAt(origin, %)
@ -244,7 +241,7 @@ However you likely want to use those tags somewhere outside the `rect` function.
Tags are accessible through the sketch group they are declared in.
For example the following code works.
```
```norun
fn rect(origin) {
return startSketchOn('XZ')
|> startProfileAt(origin, %)

View File

@ -1,4 +1,8 @@
use std::collections::{BTreeMap, HashMap};
use std::{
collections::{BTreeMap, HashMap},
fs::File,
io::Read as _,
};
use anyhow::Result;
use base64::Engine;
@ -7,15 +11,18 @@ use handlebars::Renderable;
use indexmap::IndexMap;
use itertools::Itertools;
use serde_json::json;
use tokio::task::JoinSet;
use crate::{
docs::{is_primitive, StdLibFn},
std::StdLib,
ExecutorContext,
};
use super::kcl_doc::{ConstData, DocData, FnData};
const TYPES_DIR: &str = "../../../docs/kcl/types";
const LANG_TOPICS: [&str; 4] = ["Types", "Modules", "Settings", "Known Issues"];
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
let mut hbs = handlebars::Handlebars::new();
@ -345,7 +352,18 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
.collect();
sorted.sort_by(|t1, t2| t1.0.cmp(&t2.0));
let data: Vec<_> = sorted.into_iter().map(|(_, val)| val).collect();
let topics: Vec<_> = LANG_TOPICS
.iter()
.map(|name| {
json!({
"name": name,
"file_name": name.to_lowercase().replace(' ', "-"),
})
})
.collect();
let data = json!({
"lang_topics": topics,
"modules": data,
});
@ -991,3 +1009,67 @@ fn test_generate_stdlib_json_schema() {
&serde_json::to_string_pretty(&json_data).unwrap(),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_code_in_topics() {
let mut join_set = JoinSet::new();
for name in LANG_TOPICS {
let filename = format!("../../../docs/kcl/{}.md", name.to_lowercase().replace(' ', "-"));
let mut file = File::open(&filename).unwrap();
let mut text = String::new();
file.read_to_string(&mut text).unwrap();
for (i, (eg, attr)) in find_examples(&text, &filename).into_iter().enumerate() {
if attr == "norun" {
continue;
}
let f = filename.clone();
join_set.spawn(async move { (format!("{f}, example {i}"), run_example(&eg).await) });
}
}
let results: Vec<_> = join_set
.join_all()
.await
.into_iter()
.filter_map(|a| a.1.err().map(|e| format!("{}: {}", a.0, e)))
.collect();
assert!(results.is_empty(), "Failures: {}", results.join(", "))
}
fn find_examples(text: &str, filename: &str) -> Vec<(String, String)> {
let mut buf = String::new();
let mut attr = String::new();
let mut in_eg = false;
let mut result = Vec::new();
for line in text.lines() {
if let Some(rest) = line.strip_prefix("```") {
if in_eg {
result.push((buf, attr));
buf = String::new();
attr = String::new();
in_eg = false;
} else {
attr = rest.to_owned();
in_eg = true;
}
continue;
}
if in_eg {
buf.push('\n');
buf.push_str(line)
}
}
assert!(!in_eg, "Unclosed code tags in {}", filename);
result
}
async fn run_example(text: &str) -> Result<()> {
let program = crate::Program::parse_no_errs(text)?;
let ctx = ExecutorContext::new_with_default_client(crate::UnitLength::Mm).await?;
let mut exec_state = crate::execution::ExecState::new(&ctx.settings);
ctx.run(&program, &mut exec_state).await?;
Ok(())
}

View File

@ -6,9 +6,14 @@ layout: manual
## Table of Contents
* [Types](kcl/types)
* [Modules](kcl/modules)
* [Known Issues](kcl/KNOWN-ISSUES)
### Language
{{#each lang_topics}}
* [`{{name}}`](kcl/{{file_name}})
{{/each}}
### Standard library
{{#each modules}}
* **`{{name}}`**
{{#each functions}}

View File

@ -65,7 +65,7 @@ impl UnitLen {
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `inch`, `ft`, `yd`"
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
),
source_ranges: vec![source_range],
})),