Compare commits

...

31 Commits

Author SHA1 Message Date
3fcdf845c1 merge main 2025-04-25 15:31:54 +01:00
0ef1483e11 Stabilize test by allowing more time before clicking object (#6495)
stabilize test by allowing more time before clicking object
2025-04-25 08:55:32 -04:00
bbaaf86e4d pre commit fmt (#6470)
* setup

* remove pre-push

* undo weird fmt
2025-04-25 04:58:58 -04:00
6a0e10f8ab Test all platforms hourly (#6494) 2025-04-25 04:58:13 -04:00
5c0ca52291 Foreign imports go thru parallelization too (#6488)
* start of parallelizing forign imports

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove_printlns

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove shit that doesnt work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove shit that doesnt work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* put back

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* multiple

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove println

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clone docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add whole module import docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add whole module import docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-25 03:25:02 +00:00
bff13f6bfe Opt-in tests that cover Windows-specific capabilities (#6489)
* Opt-in tests that cover Windows-specific capabilities

* Remove unnecessary quotes
2025-04-25 00:50:26 +00:00
6001b71f06 Add Open in New Window and reexecution on import change (#6379)
* Quick prototype: open in new window in file tree

* WIP: refresh on imported file change

* Fix up reexecution

* Clean up

* Add test 'Assembly gets reexecuted when imported models are updated externally'

* Clean up
2025-04-25 09:02:18 +10:00
bd1e68a4c8 turn on fillets UI in prod (#6474)
* turn on fillets

* delete status property re-Pierre

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-04-24 17:22:28 -04:00
8589f8fc5f Opt-in tests that cover macOS-specific capabilities (#6486) 2025-04-24 17:15:11 -04:00
6641e1178b bump three point arc to be the default (#6464)
* bump three point arc up

* Update tests to select tangential arc from dropdown

* Fix another test that used tangential arc

* Fix up snapshot tests' use of Tangential Arc

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-25 06:40:33 +10:00
6e0f1e71b2 add a test for coercing a nested array (#6326)
* add a test for coercing a nested array

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add console log

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add console log

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add test for what we get back on no_outputs error

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* remove my log

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* move test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Paul Tagliamonte <paul@zoo.dev>
2025-04-24 19:37:44 +00:00
0e945b6457 properly close the context (#6485)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-24 19:37:22 +00:00
19155a9132 Fix logout issue on desktop (#6450)
* WIP: Fix logout issue on desktop
Fixes #5640

* Fix test with addInitScript
2025-04-24 15:29:17 -04:00
305d613d40 Nadro/adhoc/system io machine (#6352)
* chore: saving off skeleton

* fix: saving skeleton

* chore: skeleton for loading projects from project directory path

* chore: cleaning up useless state transition to be an on event direct to action state

* fix: new structure for web vs desktop vs react machine provider code

* chore: saving off skeleton

* fix: skeleton logic for react? going to move it from a string to obj.string

* fix: trying to prevent error element unmount on global react components. This is bricking JS state

* fix: we are so back

* chore: implemented navigating to specfic KCL file

* chore: implementing renaming project

* chore: deleting project

* fix: auto fixes

* fix: old debug/testing file oops

* chore: generic create new file

* chore: skeleton for web create file provide

* chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest?

* chore: save off progress before deleting other project implementation, a few missing features still

* chore: trying a different init skeleton? most likely will migrate

* chore: first attempt of purging projects context provider

* chore: enabling toast for some machine state

* chore: enabling more toast success and error

* chore: writing read write state to the system io based on the project path

* fix: tsc fixes

* fix: use file system watcher, navigate to project after creation via the requestProjectName

* chore: open project command, hooks vs snapshot context helpers

* chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now.

* fix: codespell fixes

* chore: implementing more project commands

* chore: PR improvements for root.tsx

* chore: leaving comment about new Router.tsx layout

* fix: removing debugging code

* fix: rewriting component for readability

* fix: improving web initialization

* chore: implementing import file from url which is not actually that?

* fix: clearing search params on import file from url

* fix: fixed two e2e tests, forgot needsReview when making new command

* fix: fixing some import from url business logic to pass e2e tests

* chore: script for diffing circular deps +/-

* fix: formatting

* fix: massive fix for circular depsga!

* fix: trying to fix some errors and auto fmt

* fix: updating deps

* fix: removing debugging code

* fix: big clean up

* fix: more deletion

* fix: tsc cleanup

* fix: TSC TSC TSC TSC!

* fix: typo fix

* fix: clear query params on web only, desktop not required

* fix: removing unused code

* fmt

* Bring back `trap` removed in merge

* Use explicit types instead of `any`s on arg configs

* Add project commands directly to command palette

* fix: deleting debugging code, from PR review

* fix: this got added back(?)

* fix: using referred type

* fix: more PR clean up

* fix: big block comment for xstate architecture decision

* fix: more pr comment fixes

* fix: merge conflict just added them back why dude

* fix: more PR comments

* fix: big ciruclar deps fix, commandBarActor in appActor

---------

Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-04-24 13:32:49 -05:00
95f2caacab Fix testing-selections test / clicking on empty space (#6481)
fix clicking on empty space which happened to click on UI, causing the test to fail
2025-04-24 17:46:07 +00:00
8f61ee1d2f Change getOppositeEdge, getNextAdjacentEdge, and getPreviousAdjacentEdge to keyword args (#6469)
* Change getOppositeEdge, getNextAdjacentEdge, and getPreviousAdjacentEdge to keyword args

* Update generated docs
2025-04-24 12:39:37 -04:00
b0594712f8 update std.json 2025-04-24 16:50:32 +01:00
a3821f6229 merge main 2025-04-24 16:31:30 +01:00
b02dbd4fe6 Kwargs: leg helpers (#6459)
legLen, legAngX, legAngY moved to keyword arguments
2025-04-24 09:53:19 -05:00
668f6671a9 BREAKING: Remove angleToMatchLengthX and angleToMatchLengthY (#6451)
* Remove angleToMatchLengthX and angleToMatchLengthY from constraint code generation

* Change KCL stdlib functions to be deprecated

* Remove references from TS tests

* Remove angleToMatchLengthX and angleToMatchLengthY

* Update docs

* Fix file paths
2025-04-24 14:33:27 +00:00
f6387eb7e9 fmt (#6473) 2025-04-24 13:10:06 +00:00
83a87b046f Declare std::offsetPlane in KCL (#6344)
* Declare std::offsetPlane in KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Use two axes to define planes in KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-04-24 22:01:27 +12:00
20c2ce3bac Shorten tronapp setup fail (#6362)
* shorten tron app setup fail

* fmt

* more tweaks to tronapp setup

* bump initial timeout back to 120s

* Update e2e/playwright/zoo-test.ts

---------

Co-authored-by: Zookeeper Lee <lee@zoo.dev>
2025-04-24 06:35:57 +00:00
2956f9ed55 YOU FOOLS I WON THE CONTEST (#6328)
* dodec

* fmt

* comment

* Update kcl-samples simulation test output

* Update kcl-samples simulation test output

* Fix so that just commands regenerate ast output

* overwrite

* Update just command to include manifest

* Update generated output

* merge main post

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-04-24 06:08:45 +00:00
510d74f2c7 Add clone (#5462)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

update the extrude idds

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix sample

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

fix the start and end tag

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

new tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

codespell

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix examples

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix some stuff

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add another test for fillet

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update rust/kcl-lib/src/std/clone.rs

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add sweep test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* revolve test;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Update rust/kcl-lib/src/std/clone.rs

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* add another test for fillet

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* allow cloning an imported geometry;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* allow for imported geometry

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-04-24 04:26:09 +00:00
457ab28f74 Appearance import fixes (#6466)
* fix appearance imports

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix appearance

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-24 00:43:16 +00:00
42082f72cd new simulation test for involute 2025-04-23 18:26:45 +01:00
55baf58682 redo docs 2025-04-23 18:20:58 +01:00
02bd211b70 fmt 2025-04-23 17:33:51 +01:00
37d34d45ff add check for end radius being larger 2025-04-23 17:29:09 +01:00
ed774e67b2 Add involute to Path info 2025-04-23 16:54:39 +01:00
399 changed files with 91758 additions and 45563 deletions

View File

@ -289,17 +289,6 @@ jobs:
- windows-latest-8-cores
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
# Disable macos and windows tests on hourly e2e tests since we only care
# about server side changes.
# Technique from https://github.com/joaomcteixeira/python-project-skeleton/pull/31/files
isScheduled:
- ${{ github.event_name == 'schedule' }}
exclude:
- os: namespace-profile-macos-8-cores
isScheduled: true
- os: windows-latest-8-cores
isScheduled: true
# TODO: add ref here for main and latest release tag
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

1
.husky/pre-commit Executable file
View File

@ -0,0 +1 @@
npm run fmt

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run fmt-check

File diff suppressed because one or more lines are too long

258
docs/kcl/clone.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ Get the next adjacent edge to the edge given.
```js
getNextAdjacentEdge(tag: TagIdentifier): Uuid
getNextAdjacentEdge(edge: TagIdentifier): Uuid
```
@ -17,7 +17,7 @@ getNextAdjacentEdge(tag: TagIdentifier): Uuid
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| [`tag`](/docs/kcl/types/tag) | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes |
| `edge` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The tag of the edge you want to find the next adjacent edge of. | Yes |
### Returns

View File

@ -9,7 +9,7 @@ Get the opposite edge to the edge given.
```js
getOppositeEdge(tag: TagIdentifier): Uuid
getOppositeEdge(edge: TagIdentifier): Uuid
```
@ -17,7 +17,7 @@ getOppositeEdge(tag: TagIdentifier): Uuid
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| [`tag`](/docs/kcl/types/tag) | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes |
| `edge` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The tag of the edge you want to find the opposite edge of. | Yes |
### Returns

View File

@ -9,7 +9,7 @@ Get the previous adjacent edge to the edge given.
```js
getPreviousAdjacentEdge(tag: TagIdentifier): Uuid
getPreviousAdjacentEdge(edge: TagIdentifier): Uuid
```
@ -17,7 +17,7 @@ getPreviousAdjacentEdge(tag: TagIdentifier): Uuid
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| [`tag`](/docs/kcl/types/tag) | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | | Yes |
| `edge` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The tag of the edge you want to find the previous adjacent edge of. | Yes |
### Returns

View File

@ -32,8 +32,6 @@ layout: manual
* [`Z`](kcl/consts/std-Z)
* [`abs`](kcl/abs)
* [`acos`](kcl/acos)
* [`angleToMatchLengthX`](kcl/angleToMatchLengthX)
* [`angleToMatchLengthY`](kcl/angleToMatchLengthY)
* [`angledLine`](kcl/angledLine)
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`appearance`](kcl/appearance)
@ -47,6 +45,7 @@ layout: manual
* [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer)
* [`circleThreePoint`](kcl/circleThreePoint)
* [`clone`](kcl/clone)
* [`close`](kcl/close)
* [`extrude`](kcl/extrude)
* [`fillet`](kcl/fillet)
@ -74,7 +73,7 @@ layout: manual
* [`map`](kcl/map)
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`offsetPlane`](kcl/offsetPlane)
* [`offsetPlane`](kcl/std-offsetPlane)
* [`patternCircular2d`](kcl/patternCircular2d)
* [`patternCircular3d`](kcl/patternCircular3d)
* [`patternLinear2d`](kcl/patternLinear2d)

View File

@ -22,6 +22,5 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.
Changing the appearance on an imported model does not work.
- **CSG Booleans**: Coplanar (bodies that share a plane) unions, subtractions, and intersections are not currently supported.

View File

@ -24,8 +24,8 @@ legAngX(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl/types/number) | | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | | Yes |
| `hypotenuse` | [`number`](/docs/kcl/types/number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
@ -35,7 +35,7 @@ legAngX(
### Examples
```js
legAngX(5, 3)
legAngX(hypotenuse = 5, leg = 3)
```

View File

@ -24,8 +24,8 @@ legAngY(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl/types/number) | | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | | Yes |
| `hypotenuse` | [`number`](/docs/kcl/types/number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
@ -35,7 +35,7 @@ legAngY(
### Examples
```js
legAngY(5, 3)
legAngY(hypotenuse = 5, leg = 3)
```

View File

@ -24,8 +24,8 @@ legLen(
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl/types/number) | | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | | Yes |
| `hypotenuse` | [`number`](/docs/kcl/types/number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl/types/number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
@ -35,7 +35,7 @@ legLen(
### Examples
```js
legLen(5, 3)
legLen(hypotenuse = 5, leg = 3)
```

View File

@ -10,7 +10,7 @@ isolated from other files as a separate module.
When you define a function, you can use `export` before it to make it available
to other modules.
```
```kcl
// util.kcl
export fn increment(x) {
return x + 1
@ -31,11 +31,11 @@ Imported files _must_ be in the same project so that units are uniform across
modules. This means that it must be in the same directory.
Import statements must be at the top-level of a file. It is not allowed to have
an `import` statement inside a function or in the body of an if-else.
an `import` statement inside a function or in the body of an ifelse.
Multiple functions can be exported in a file.
```
```kcl
// util.kcl
export fn increment(x) {
return x + 1
@ -58,6 +58,211 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
import increment as inc, decrement as dec from "util.kcl"
```
---
## Functions vs `clone`
There are two common patterns for reusing geometry:
1. **Wrap the construction in a function** flexible and fully parametric.
2. **Duplicate an existing object with `clone`** lightningfast, but an exact
duplicate.
### Parametric function example
```kcl
fn cube(center) {
return startSketchOn(XY)
|> startProfileAt([center[0] - 10, center[1] - 10], %)
|> line(endAbsolute = [center[0] + 10, center[1] - 10])
|> line(endAbsolute = [center[0] + 10, center[1] + 10])
|> line(endAbsolute = [center[0] - 10, center[1] + 10])
|> close()
|> extrude(length = 10)
}
myCube = cube([0, 0])
```
*Pros*
- Any argument can be a parameter size, position, appearance, etc.
- Works great inside loops, arrays, or optimisation sweeps.
*Cons*
- Every invocation rebuilds the entire feature tree.
- **Slower** than a straight duplicate each call is its own render job.
### `clone` example
```kcl
sketch001 = startSketchOn(-XZ)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = 5)
|> appearance(color = "#ff0000", metalness = 90, roughness = 90)
sketch002 = clone(sketch001) // ✓ instant copy
```
*Pros*
- Roughly an O(1) operation we just duplicate the underlying engine handle.
- Perfect when you need ten identical bolts or two copies of the same imported STEP file.
*Cons*
- **Not parametric** the clone is exactly the same shape as the source.
- If you need to tweak dimensions perinstance, youre back to a function.
> **Rule of thumb** Reach for `clone` when the geometry is already what you want. Reach for a function when you need customisation.
---
## Modulelevel parallelism
Under the hood, the Design Studio runs **every module in parallel** where it can. This means:
- The toplevel code of `foo.kcl`, `bar.kcl`, and `baz.kcl` all start executing immediately and concurrently.
- Imports that read foreign files (STEP/OBJ/…) overlap their I/O and background render.
- CPUbound calculations in separate modules get their own worker threads.
### Why modules beat onebigfile
If you shoehorn everything into `main.kcl`, each statement runs sequentially:
```norun
import "big.step" as gizmo // blocks main while reading
gizmo |> translate(x=50) // blocks again while waiting for render
```
Split `gizmo` into its own file and the read/render can overlap whatever else `main.kcl` is doing.
```norun
// gizmo.kcl (worker A)
import "big.step"
// main.kcl (worker B)
import "gizmo.kcl" as gizmo // nonblocking
// ... other setup ...
gizmo |> translate(x=50) // only blocks here
```
### Gotcha: defining but **not** calling functions
Defining a function inside a module is instantaneous we just record the bytecode. The heavy lifting happens when the function is **called**. So:
```norun
// util.kcl
export fn makeBolt(size) { /* … expensive CAD … */ }
```
If `main.kcl` waits until the very end to call `makeBolt`, *none* of that work was parallelised youve pushed the cost back onto the serial tail of your script.
**Better:** call it early or move the invocation into another module.
```norun
// bolt_instance.kcl
import makeBolt from "util.kcl"
bolt = makeBolt(5) // executed in parallel
bolt
```
Now `main.kcl` can `import "bolt_instance.kcl" as bolt` and get the result that was rendered while it was busy doing other things.
---
## Whole module import
You can also import the whole module. This is useful if you want to use the
result of a module as a variable, like a part.
```norun
import "tests/inputs/cube.kcl" as cube
cube
|> translate(x=10)
```
This imports the whole module and makes it available as `cube`. You can then
use it like any other object. The `cube` variable is now a reference to the
result of the module. This means that if you change the module, the `cube`
variable will change as well.
In `cube.kcl`, you cannot have multiple objects. It has to be a single part. If
you have multiple objects, you will get an error. This is because the module is
expected to return a single object that can be used as a variable.
You also cannot assign that object to a variable. This is because the module is
expected to return a single object that can be used as a variable.
So for example, this is not allowed:
```norun
... a bunch of code to create cube and cube2 ...
myUnion = union([cube, cube2])
```
What you need to do instead is:
```norun
... a bunch of code to create cube and cube2 ...
union([cube, cube2])
```
That way the last line will return the union of the two objects.
Or what you could do instead is:
```norun
... a bunch of code to create cube and cube2 ...
myUnion = union([cube, cube2])
myUnion
```
This will return the union of the two objects, but it will not be assigned to a
variable. This is because the module is expected to return a single object that
can be used as a variable.
---
## Multiple instances of the same import
Whether you are importing a file from another CAD system or a KCL file, that
file represents object(s) in memory. If you import the same file multiple times,
it will only be rendered once.
If you want to have multiple instances of the same object, you can use the
[`clone`](/docs/kcl/clone) function. This will render a new instance of the object in memory.
```norun
import cube from "tests/inputs/cube.kcl"
cube
|> translate(x=10)
clone(cube)
|> translate(x=20)
```
In the sample above, the `cube` object is imported from a KCL file. The first
instance is translated 10 units in the x direction. The second instance is
cloned and translated 20 units in the x direction. The two instances are now
separate objects in memory, and can be manipulated independently.
Here is an example with a file from another CAD system:
```kcl
import "tests/inputs/cube.step" as cube
cube
|> translate(x=10)
clone(cube)
|> translate(x=20)
```
---
## Importing files from other CAD systems
`import` can also be used to import files from other CAD systems. The format of the statement is the
@ -69,25 +274,17 @@ import "tests/inputs/cube.obj"
// Use `cube` just like a KCL object.
```
```norun
import "tests/inputs/cube-2.sldprt" as cube
```kcl
import "tests/inputs/cube.sldprt" as cube
// Use `cube` just like a KCL object.
```
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"
```
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.,
by using an attribute. Likewise, you can also specify a coordinate system. E.g.,
```norun
```kcl
@(unitLength = ft, coords = opengl)
import "tests/inputs/cube.obj"
```
@ -110,97 +307,55 @@ Coordinate systems:
- `opengl`, forward: +Z, up: +Y, handedness: right
- `vulkan`, forward: +Z, up: -Y, handedness: left
### Performance
---
Parallelized foreign-file imports now let you overlap file reads, initialization,
## Performance deepdive for foreignfile imports
Parallelized foreignfile imports now let you overlap file reads, initialization,
and rendering. To maximize throughput, you need to understand the three distinct
stages—reading, initializing (background render start), and invocation (blocking)
—and structure your code to defer blocking operations until the end.
#### Foreign Import Execution Stages
### Foreign import execution stages
1. **Import (Read) Stage**
```norun
1. **Import (Read / Initialization) Stage**
```kcl
import "tests/inputs/cube.step" as cube
```
- Reads the file from disk and makes its API available.
- **Does _not_** start Engine rendering or block your script.
- Starts engine rendering but **does not block** your script.
- This kickstarts the render pipeline while you keep executing other code.
2. **Initialization (Background Render) Stage**
```norun
2. **Invocation (Blocking) Stage**
```kcl
import "tests/inputs/cube.step" as cube
myCube = cube // <- This line starts background rendering
```
- Invoking the imported symbol (assignment or plain call) triggers Engine rendering _in the background_.
- This kickstarts the render pipeline but doesnt block—you can continue other work while the Engine processes the model.
3. **Invocation (Blocking) Stage**
```norun
import "tests/inputs/cube.step" as cube
myCube = cube
myCube
|> translate(z=10) // <- This line blocks
cube
|> translate(z=10) // ← blocks here only
```
- Any method call (e.g., `translate`, `scale`, `rotate`) waits for the background render to finish before applying transformations.
- This is the only point where your script will block.
> **Nuance:** Foreign imports differ from pure KCL modules—calling the same import symbol multiple times (e.g., `screw` twice) starts background rendering twice.
### Best practices
#### Best Practices
#### 1. Defer blocking calls
##### 1. Defer Blocking Calls
Initialize early but delay all transformations until after your heavy computation:
```norun
import "tests/inputs/cube.step" as cube // 1) Read
myCube = cube // 2) Background render starts
```kcl
import "tests/inputs/cube.step" as cube // 1) Read / Background render starts
// --- perform other operations and calculations or setup here ---
// --- perform other operations and calculations here ---
myCube
|> translate(z=10) // 3) Blocks only here
```
##### 2. Encapsulate Imports in Modules
Keep `main.kcl` free of reads and initialization; wrap them:
```norun
// imports.kcl
import "tests/inputs/cube.step" as cube // Read only
export myCube = cube // Kick off rendering
```
```norun
// main.kcl
import myCube from "imports.kcl" // Import the initialized object
// ... computations ...
myCube
|> translate(z=10) // Blocking call at the end
```
##### 3. Avoid Immediate Method Calls
```norun
import "tests/inputs/cube.step" as cube
cube
|> translate(z=10) // Blocks immediately, negating parallelism
|> translate(z=10) // 2) Blocks only here
```
Both calling methods right on `cube` immediately or leaving an implicit import without assignment introduce blocking.
#### 2. Split heavy work into separate modules
#### Future Improvements
Place computationally expensive or IOheavy work into its own module so it can render in parallel while `main.kcl` continues.
#### Future improvements
Upcoming releases will autoanalyse dependencies and only block when truly necessary. Until then, explicit deferral will give you the best performance.
Upcoming releases will autoanalyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.

File diff suppressed because one or more lines are too long

122
docs/kcl/std-offsetPlane.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,26 @@ An extruded arc.
| `sourceRange` |`[integer, integer, integer]`| The source range. | No |
----
An extruded involute.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `extrudeInvolute`| | No |
| `faceId` |[`string`](/docs/kcl/types/string)| The face id for the extrude surface. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag. | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the geometry. | No |
| `sourceRange` |`[integer, integer, integer]`| The source range. | No |
----
Geometry metadata.

View File

@ -0,0 +1,84 @@
---
title: "GeometryWithImportedGeometry"
excerpt: "A geometry including an imported geometry."
layout: manual
---
A geometry including an imported geometry.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Sketch`](/docs/kcl/types/Sketch)| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the sketch (this will change when the engine's reference to it changes). | No |
| `paths` |`[` [`Path`](/docs/kcl/types/Path) `]`| The paths in the sketch. | No |
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `artifactId` |[`string`](/docs/kcl/types/string)| The original id of the sketch. This stays the same even if the sketch is is sketched on face etc. | No |
| `originalId` |[`string`](/docs/kcl/types/string)| | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
| `id` |[`string`](/docs/kcl/types/string)| The id of the solid. | No |
| `artifactId` |[`string`](/docs/kcl/types/string)| The artifact ID of the solid. Unlike `id`, this doesn't change. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |[`number`](/docs/kcl/types/number)| The height of the solid. | No |
| `startCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion start cap | No |
| `endCapId` |[`string`](/docs/kcl/types/string)| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| The units of the solid. | No |
| `sectional` |`boolean`| Is this a sectional solid? | No |
----
Data for an imported geometry.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportedGeometry`| | No |
| `id` |[`string`](/docs/kcl/types/string)| The ID of the imported geometry. | No |
| `value` |`[` [`string`](/docs/kcl/types/string) `]`| The original file paths. | No |
----

View File

@ -241,6 +241,31 @@ A circular arc, not necessarily tangential to the current point.
----
An involute of a circle of start_radius ending at end_radius
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CircularInvolute`| | No |
| `start_radius` |[`number`](/docs/kcl/types/number)| The radius of the base circle of the involute | No |
| `end_radius` |[`number`](/docs/kcl/types/number)| The radius that the involute ends at | No |
| `angle` |[`number`](/docs/kcl/types/number)| Angle about which the whole involute is rotated | No |
| `reverse` |`boolean`| If true, the path segment starts at the end radius and goes towards the start radius | No |
| `from` |`[number, number]`| The from point. | No |
| `to` |`[number, number]`| The to point. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
----

View File

@ -30,7 +30,6 @@ A sketch type.
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
----
@ -52,7 +51,6 @@ A face.
| `value` |[`string`](/docs/kcl/types/string)| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face's Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A unit of length. | No |

View File

@ -13,12 +13,22 @@ test.describe('Authentication tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor()
// This is only needed as an override to test-utils' setup() for this test
await page.addInitScript(() => {
localStorage.setItem('TOKEN_PERSIST_KEY', '')
})
await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step("Refresh doesn't log the user back in", async () => {
await page.reload()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
@ -30,6 +40,7 @@ test.describe('Authentication tests', () => {
await expect(signInPage.userCode).toBeVisible()
const secondUserCode = await signInPage.userCode.textContent()
expect(secondUserCode).not.toEqual(firstUserCode)
await signInPage.cancelSignInButton.click()
})
await test.step('Press back button and remain on home page', async () => {
@ -48,6 +59,12 @@ test.describe('Authentication tests', () => {
// Longer timeout than usual here for the wait on home page
await expect(homePage.projectSection).toBeVisible({ timeout: 10000 })
})
await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible()
})
}
)
})

View File

@ -155,7 +155,7 @@ async function doBasicSketch(
|> xLine(length = -segLen(seg01))`)
}
test.describe('Basic sketch', { tag: ['@skipWin'] }, () => {
test.describe('Basic sketch', () => {
test('code pane open at start', async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
await doBasicSketch(page, homePage, ['code'])

View File

@ -8,130 +8,126 @@ import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe(
'Can create sketches on all planes and their back sides',
{ tag: ['@skipWin'] },
() => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
scene: SceneFixture,
toolbar: ToolbarFixture,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
scene: SceneFixture,
toolbar: ToolbarFixture,
plane: string,
clickCoords: { x: number; y: number }
) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
const XYPlanRed: [number, number, number] = [98, 50, 51]
await scene.expectPixelColor(XYPlanRed, { x: 700, y: 300 }, 15)
await homePage.goToModelingScene()
const XYPlanRed: [number, number, number] = [98, 50, 51]
await scene.expectPixelColor(XYPlanRed, { x: 700, y: 300 }, 15)
await u.openDebugPanel()
await u.openDebugPanel()
const coord =
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: coord, y: coord, z: coord },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
const code = `@settings(defaultLengthUnit = in)sketch001 = startSketchOn(${plane})profile001 = startProfileAt([0.91, -1.22], sketch001)`
await u.openDebugPanel()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(600) // wait for animation
await toolbar.waitUntilSketchingReady()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toBeVisible()
await u.closeDebugPanel()
await page.mouse.click(707, 393)
await expect(page.locator('.cm-content')).toHaveText(code)
await page
.getByRole('button', { name: 'line Line', exact: true })
.first()
.click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
const coord =
plane === '-XY' || plane === '-YZ' || plane === 'XZ' ? -100 : 100
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 0, y: 0, z: 0 },
vantage: { x: coord, y: coord, z: coord },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
const planeConfigs = [
{
plane: 'XY',
coords: { x: 600, y: 388 },
description: 'red plane',
},
{
plane: 'YZ',
coords: { x: 700, y: 250 },
description: 'green plane',
},
{
plane: 'XZ',
coords: { x: 684, y: 427 },
description: 'blue plane',
},
{
plane: '-XY',
coords: { x: 600, y: 118 },
description: 'back of red plane',
},
{
plane: '-YZ',
coords: { x: 700, y: 219 },
description: 'back of green plane',
},
{
plane: '-XZ',
coords: { x: 700, y: 80 },
description: 'back of blue plane',
},
]
const code = `@settings(defaultLengthUnit = in)sketch001 = startSketchOn(${plane})profile001 = startProfileAt([0.91, -1.22], sketch001)`
for (const config of planeConfigs) {
test(config.plane, async ({ page, homePage, scene, toolbar }) => {
await sketchOnPlaneAndBackSideTest(
page,
homePage,
scene,
toolbar,
config.plane,
config.coords
)
})
}
await u.openDebugPanel()
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await u.closeDebugPanel()
await page.mouse.click(clickCoords.x, clickCoords.y)
await page.waitForTimeout(600) // wait for animation
await toolbar.waitUntilSketchingReady()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).toBeVisible()
await u.closeDebugPanel()
await page.mouse.click(707, 393)
await expect(page.locator('.cm-content')).toHaveText(code)
await page
.getByRole('button', { name: 'line Line', exact: true })
.first()
.click()
await u.openAndClearDebugPanel()
await page.getByRole('button', { name: 'Exit Sketch' }).click()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearCommandLogs()
await u.removeCurrentCode()
}
)
const planeConfigs = [
{
plane: 'XY',
coords: { x: 600, y: 388 },
description: 'red plane',
},
{
plane: 'YZ',
coords: { x: 700, y: 250 },
description: 'green plane',
},
{
plane: 'XZ',
coords: { x: 684, y: 427 },
description: 'blue plane',
},
{
plane: '-XY',
coords: { x: 600, y: 118 },
description: 'back of red plane',
},
{
plane: '-YZ',
coords: { x: 700, y: 219 },
description: 'back of green plane',
},
{
plane: '-XZ',
coords: { x: 700, y: 80 },
description: 'back of blue plane',
},
]
for (const config of planeConfigs) {
test(config.plane, async ({ page, homePage, scene, toolbar }) => {
await sketchOnPlaneAndBackSideTest(
page,
homePage,
scene,
toolbar,
config.plane,
config.coords
)
})
}
})

View File

@ -10,7 +10,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
test.describe('Code pane and errors', () => {
test('Typing KCL errors induces a badge on the code pane button', async ({
page,
homePage,

View File

@ -9,7 +9,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
page,
homePage,
@ -179,57 +179,57 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
await expect(commandLevelArgButton).toHaveText('level: project')
})
test(
'Command bar keybinding works from code editor and can change a setting',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
test('Command bar keybinding works from code editor and can change a setting', async ({
page,
homePage,
}) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// FIXME: No KCL code, unable to wait for engine execution
await page.waitForTimeout(10000)
// FIXME: No KCL code, unable to wait for engine execution
await page.waitForTimeout(10000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
// Put the cursor in the code editor
await page.locator('.cm-content').click()
// Put the cursor in the code editor
await page.locator('.cm-content').click()
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('ControlOrMeta+K')
// Now try the same, but with the keyboard shortcut, check focus
await page.keyboard.press('ControlOrMeta+K')
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
let cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
await expect(cmdSearchBar).toBeFocused()
// Try typing in the command bar
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('dark')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(
page.getByRole('option', { name: 'system' })
).toHaveAttribute('data-headlessui-state', 'active')
await page.keyboard.press('Enter')
// Try typing in the command bar
await cmdSearchBar.fill('theme')
const themeOption = page.getByRole('option', {
name: 'Settings · app · theme',
})
await expect(themeOption).toBeVisible()
await themeOption.click()
const themeInput = page.getByPlaceholder('dark')
await expect(themeInput).toBeVisible()
await expect(themeInput).toBeFocused()
// Select dark theme
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" as a user default`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}
)
// Check the toast appeared
await expect(
page.getByText(`Set theme to "system" as a user default`)
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
})
test('Can extrude from the command bar', async ({
page,

View File

@ -10,7 +10,7 @@ import { expect, test } from '@e2e/playwright/zoo-test'
test(
'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] },
{ tag: ['@electron', '@macos', '@windows', '@skipLocalEngine'] },
async ({ page, context, scene, tronApp, cmdBar }, testInfo) => {
if (!tronApp) {
fail()

View File

@ -10,7 +10,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
@ -989,162 +989,162 @@ sketch001 = startSketchOn(XZ)
|> close()`)
})
test(
'Can undo a sketch modification with ctrl+z',
{ tag: ['@skipWin'] },
async ({ page, homePage, editor }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
test('Can undo a sketch modification with ctrl+z', async ({
page,
homePage,
editor,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`@settings(defaultLengthUnit=in)
sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`
)
})
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await homePage.goToModelingScene()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await page.waitForTimeout(100)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 0, y: -1250, z: 580 },
center: { x: 0, y: 0, z: 0 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
const startPX = [1200 / 2, 500 / 2]
const startPX = [1200 / 2, 500 / 2]
const dragPX = 40
const dragPX = 40
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await page.getByText('startProfileAt([4.61, -10.01], %)').click()
await expect(
page.getByRole('button', { name: 'Edit Sketch' })
).toBeVisible()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(400)
let prevContent = await page.locator('.cm-content').innerText()
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
await expect(page.getByTestId('segment-overlay')).toHaveCount(2)
// drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag startProfileAt handle
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: startPX[0] + 68, y: startPX[1] + 147 },
targetPosition: { x: startPX[0] + dragPX, y: startPX[1] + dragPX },
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// drag line handle
// we wait so it saves the code
await page.waitForTimeout(800)
// drag line handle
// we wait so it saves the code
await page.waitForTimeout(800)
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]')
await page.waitForTimeout(100)
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: lineEnd.x - 5, y: lineEnd.y },
targetPosition: { x: lineEnd.x + dragPX, y: lineEnd.y + dragPX },
})
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText()
// we wait so it saves the code
await page.waitForTimeout(800)
// we wait so it saves the code
await page.waitForTimeout(800)
// drag tangentialArc handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// drag tangentialArc handle
const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]')
await page.dragAndDrop('#stream', '#stream', {
sourcePosition: { x: tangentEnd.x + 10, y: tangentEnd.y - 5 },
targetPosition: {
x: tangentEnd.x + dragPX,
y: tangentEnd.y + dragPX,
},
})
await page.waitForTimeout(100)
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
// expect the code to have changed
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
// expect the code to have changed
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78])
|> tangentialArc(endAbsolute = [27.6, -3.05])
|> close()
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
{ shouldNormalise: true }
)
// Hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %)
|> line(end = [15.4, -2.78])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
{ shouldNormalise: true }
)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfileAt([2.71, -2.71], %)
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
{ shouldNormalise: true }
)
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
// Hit undo again.
await page.keyboard.down('Control')
await page.keyboard.press('KeyZ')
await page.keyboard.up('Control')
await page.waitForTimeout(100)
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100)
await editor.expectEditor.toContain(
`sketch001 = startSketchOn(XZ)
|> startProfileAt([4.61, -10.01], %)
|> line(end = [12.73, -0.09])
|> tangentialArc(endAbsolute = [24.95, -0.38])
|> close()
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
}
)
{ shouldNormalise: true }
)
})
test(
`Can import a local OBJ file`,

View File

@ -29,7 +29,7 @@ test.describe('integrations tests', () => {
)
})
const [clickObj] = await scene.makeMouseHelpers(726, 272)
const [clickObj] = scene.makeMouseHelpers(726, 272)
await test.step('setup test', async () => {
await homePage.expectState({
@ -73,7 +73,7 @@ test.describe('integrations tests', () => {
})
await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl')
await page.waitForTimeout(1000)
await page.waitForTimeout(2000)
await clickObj()
await page.waitForTimeout(1000)
await scene.moveNoWhere()

View File

@ -174,6 +174,13 @@ export class ToolbarFixture {
openFile = async (fileName: string) => {
await this.filePane.getByText(fileName).click()
}
selectTangentialArc = async () => {
await this.page.getByRole('button', { name: 'caret down arcs:' }).click()
await expect(
this.page.getByTestId('dropdown-three-point-arc')
).toBeVisible()
await this.page.getByTestId('dropdown-tangential-arc').click()
}
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down rectangles:' })

View File

@ -41,7 +41,7 @@ class MyAPIReporter implements Reporter {
annotations: test.annotations.map((a) => a.type), // e.g. 'fail' or 'fixme'
id: test.id, // computed file/test/project ID used for reruns
retry: result.retry,
tags: test.tags, // e.g. '@snapshot' or '@skipWin'
tags: test.tags, // e.g. '@snapshot' or '@skipLocalEngine'
// Extra environment variables
CI_COMMIT_SHA: process.env.CI_COMMIT_SHA || null,
CI_PR_NUMBER: process.env.CI_PR_NUMBER || null,

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import {
executorInputPath,
getUtils,
kclSamplesPath,
testsInputPath,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
@ -472,4 +473,94 @@ test.describe('Point-and-click assemblies tests', () => {
})
}
)
test(
'Assembly gets reexecuted when imported models are updated externally',
{ tag: ['@electron'] },
async ({ context, page, homePage, scene, toolbar, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
}
const midPoint = { x: 500, y: 250 }
const washerPoint = { x: 645, y: 250 }
const partColor: [number, number, number] = [120, 120, 120]
const redPartColor: [number, number, number] = [200, 0, 0]
const bgColor: [number, number, number] = [30, 30, 30]
const tolerance = 50
const projectName = 'assembly'
await test.step('Setup parts and expect imported model', async () => {
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, projectName)
await fsp.mkdir(projectDir, { recursive: true })
await Promise.all([
fsp.copyFile(
executorInputPath('cube.kcl'),
path.join(projectDir, 'cube.kcl')
),
fsp.copyFile(
kclSamplesPath(
path.join(
'pipe-flange-assembly',
'mcmaster-parts',
'98017a257-washer.step'
)
),
path.join(projectDir, 'foreign.step')
),
fsp.writeFile(
path.join(projectDir, 'main.kcl'),
`
import "cube.kcl" as cube
import "foreign.step" as foreign
cube
foreign
|> translate(x = 40, z = 10)`
),
])
})
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.openProject(projectName)
await scene.settled(cmdBar)
await toolbar.closePane('code')
await scene.expectPixelColor(partColor, midPoint, tolerance)
})
await test.step('Change imported kcl file and expect change', async () => {
await context.folderSetupFn(async (dir) => {
// Append appearance to the cube.kcl file
await fsp.appendFile(
path.join(dir, projectName, 'cube.kcl'),
`\n |> appearance(color = "#ff0000")`
)
})
await scene.settled(cmdBar)
await toolbar.closePane('code')
await scene.expectPixelColor(redPartColor, midPoint, tolerance)
await scene.expectPixelColor(partColor, washerPoint, tolerance)
})
await test.step('Change imported step file and expect change', async () => {
await context.folderSetupFn(async (dir) => {
// Replace the washer with a pipe
await fsp.copyFile(
kclSamplesPath(
path.join(
'pipe-flange-assembly',
'mcmaster-parts',
'1120t74-pipe.step'
)
),
path.join(dir, projectName, 'foreign.step')
)
})
await scene.settled(cmdBar)
await toolbar.closePane('code')
// Expect pipe to take over the red cube but leave some space where the washer was
await scene.expectPixelColor(partColor, midPoint, tolerance)
await scene.expectPixelColor(bgColor, washerPoint, tolerance)
})
}
)
})

View File

@ -18,7 +18,7 @@ import { expect, test } from '@e2e/playwright/zoo-test'
test(
'projects reload if a new one is created, deleted, or renamed externally',
{ tag: '@electron' },
{ tag: ['@electron', '@macos', '@windows'] },
async ({ context, page }, testInfo) => {
let externalCreatedProjectName = 'external-created-project'
@ -1815,8 +1815,8 @@ test(
'basic_fillet_cube_next_adjacent.kcl',
'basic_fillet_cube_previous_adjacent.kcl',
'basic_fillet_cube_start.kcl',
'big_number_angle_to_match_length_x.kcl',
'big_number_angle_to_match_length_y.kcl',
'broken-code-test.kcl',
'circular_pattern3d_a_pattern.kcl',
'close_arc.kcl',
'computed_var.kcl',
'cube-embedded.gltf',

View File

@ -29,7 +29,7 @@ sketch003 = startSketchOn(XY)
extrude003 = extrude(sketch003, length = 20)
`
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Prompt-to-edit tests', () => {
test.describe('Check the happy path, for basic changing color', () => {
const cases = [
{

View File

@ -14,7 +14,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({
context,
@ -239,17 +239,18 @@ extrude001 = extrude(sketch001, length = 50)
await expect(zooLogo).not.toHaveAttribute('href')
})
test(
'Position _ Is Out Of Range... regression test',
{ tag: ['@skipWin'] },
async ({ context, page, homePage }) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`exampleSketch = startSketchOn("XZ")
test('Position _ Is Out Of Range... regression test', async ({
context,
page,
homePage,
}) => {
const u = await getUtils(page)
// const PUR = 400 / 37.5 //pixeltoUnitRatio
await page.setBodyDimensions({ width: 1200, height: 500 })
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine(angle = 50, length = 45 )
|> yLine(endAbsolute = 0)
@ -258,55 +259,55 @@ extrude001 = extrude(sketch001, length = 50)
example = extrude(exampleSketch, length = 5)
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
)
)
})
await expect(async () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
await page.waitForTimeout(200)
// expect it still to be there (sometimes it just clears for a bit?)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
}).toPass({ timeout: 40_000, intervals: [1_000] })
await expect(async () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
await page.waitForTimeout(200)
// expect it still to be there (sometimes it just clears for a bit?)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible({
timeout: 1_000,
})
}).toPass({ timeout: 40_000, intervals: [1_000] })
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
// Go to the end of the editor
// This bug happens when there is a diagnostic in the editor and you try to
// edit text below it.
// Or delete a huge chunk of text and then try to edit below it.
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('End')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
await expect(page.getByText('Unexpected token: |').first()).toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
// Okay execution finished, let's start editing text below the error.
await u.codeLocator.click()
// Go to the end of the editor
// This bug happens when there is a diagnostic in the editor and you try to
// edit text below it.
// Or delete a huge chunk of text and then try to edit below it.
await page.keyboard.press('End')
await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('End')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.type('thing: "blah"', { delay: 100 })
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowLeft')
await expect(
page.locator('.cm-content')
).toContainText(`exampleSketch = startSketchOn("XZ")
await expect(
page.locator('.cm-content')
).toContainText(`exampleSketch = startSketchOn("XZ")
|> startProfileAt([0, 0], %)
|> angledLine(angle = 50, length = 45 )
|> yLine(endAbsolute = 0)
@ -314,9 +315,8 @@ extrude001 = extrude(sketch001, length = 50)
thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
}
)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
})
test(
'window resize updates should reconfigure the stream',
@ -486,82 +486,81 @@ extrude002 = extrude(profile002, length = 150)
}
)
// We updated this test such that you can have multiple exports going at once.
test(
'ensure you CAN export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] },
async ({ page, homePage }) => {
const u = await getUtils(page)
await test.step('Set up the code and durations', async () => {
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{
code: bracket,
}
)
test('ensure you CAN export while an export is already going', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await test.step('Set up the code and durations', async () => {
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{
code: bracket,
}
)
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await homePage.goToModelingScene()
await u.waitForPageLoad()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
const errorToastMessage = page.getByText(`Error while exporting`)
const exportingToastMessage = page.getByText(`Exporting...`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
const successToastMessage = page.getByText(`Exported successfully`)
const errorToastMessage = page.getByText(`Error while exporting`)
const exportingToastMessage = page.getByText(`Exporting...`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
const successToastMessage = page.getByText(`Exported successfully`)
await test.step('second export', async () => {
await clickExportButton(page)
await test.step('second export', async () => {
await clickExportButton(page)
await expect(exportingToastMessage).toBeVisible()
await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page)
await clickExportButton(page)
await test.step('The first export still succeeds', async () => {
await Promise.all([
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000,
}),
])
})
})
await test.step('Successful, unblocked export', async () => {
// Try exporting again.
await clickExportButton(page)
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await test.step('The first export still succeeds', async () => {
await Promise.all([
expect(exportingToastMessage).not.toBeVisible(),
expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 }),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible({ timeout: 15_000 }),
expect(alreadyExportingToastMessage).not.toBeVisible({
timeout: 15_000,
}),
])
await expect(successToastMessage).toHaveCount(2)
})
}
)
})
await test.step('Successful, unblocked export', async () => {
// Try exporting again.
await clickExportButton(page)
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await Promise.all([
expect(exportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toHaveCount(2)
})
})
test(
`Network health indicator only appears in modeling view`,

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ test.setTimeout(60_000)
// up with another PR if we want this back.
test(
'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
{ tag: ['@snapshot'] },
async ({ page, context, scene, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
@ -464,9 +464,7 @@ test(
|> xLine(length = 184.3)`
await expect(page.locator('.cm-content')).toHaveText(code)
await page
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
await toolbar.selectTangentialArc()
// click on the end of the profile to continue it
await page.waitForTimeout(500)
@ -621,7 +619,7 @@ test.describe(
'Client side scene scale should match engine scale',
{ tag: '@snapshot' },
() => {
test('Inch scale', async ({ page, cmdBar, scene }) => {
test('Inch scale', async ({ page, cmdBar, scene, toolbar }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
@ -655,9 +653,7 @@ test.describe(
|> xLine(length = 184.3)`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
await toolbar.selectTangentialArc()
await page.waitForTimeout(100)
// click to continue profile
@ -671,9 +667,8 @@ test.describe(
await expect(u.codeLocator).toHaveText(code)
// click tangential arc tool again to unequip it
await page
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
// it will be available directly in the toolbar since it was last equipped
await toolbar.tangentialArcBtn.click()
await page.waitForTimeout(100)
// screen shot should show the sketch
@ -696,7 +691,13 @@ test.describe(
})
})
test('Millimeter scale', async ({ page, context, cmdBar, scene }) => {
test('Millimeter scale', async ({
page,
context,
cmdBar,
scene,
toolbar,
}) => {
await context.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
@ -749,9 +750,7 @@ test.describe(
|> xLine(length = 184.3)`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
await toolbar.selectTangentialArc()
await page.waitForTimeout(100)
// click to continue profile
@ -764,9 +763,7 @@ test.describe(
|> tangentialArc(endAbsolute = [551.2, -62.01])`
await expect(u.codeLocator).toHaveText(code)
await page
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click()
await toolbar.tangentialArcBtn.click()
await page.waitForTimeout(100)
// screen shot should show the sketch

View File

@ -8,228 +8,235 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Test network and connection issues', () => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
test.describe(
'Test network and connection issues',
{
tag: ['@macos', '@windows'],
},
() => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await homePage.goToModelingScene()
const networkToggle = page.getByTestId('network-toggle')
const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
}
)
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
}
)
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, toolbar, scene, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const networkToggle = page.getByTestId('network-toggle')
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage, toolbar, scene, cmdBar }) => {
test.fixme(orRunWhenFullSuiteEnabled())
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await homePage.goToModelingScene()
await u.waitForPageLoad()
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 200)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)`
)
await u.closeDebugPanel()
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)`
)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
)
await page.waitForTimeout(100)
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
await expect(
page.locator('.cm-content')
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|> xLine(length = ${commonPoints.num1})`)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await scene.settled(cmdBar)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await scene.settled(cmdBar)
// Click off the code pane.
await page.mouse.click(100, 100)
// Click off the code pane.
await page.mouse.click(100, 100)
// select a line
await page
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
.click()
// select a line
await page
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
.click()
// enter sketch again
await toolbar.editSketch()
// enter sketch again
await toolbar.editSketch()
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
// Click the line tool
await page
.getByRole('button', { name: 'line Line', exact: true })
.click()
await page.waitForTimeout(150)
await page.waitForTimeout(150)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await toolbar.openPane('debug')
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
const camCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
center: { x: 109, y: 0, z: -152 },
vantage: { x: 115, y: -505, z: -152 },
up: { x: 0, y: 0, z: 1 },
},
}
const updateCamCommand: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
}
await toolbar.openPane('debug')
await u.sendCustomCmd(camCommand)
await page.waitForTimeout(100)
await u.sendCustomCmd(updateCamCommand)
await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
// click to continue profile
await page.mouse.click(1007, 400)
await page.waitForTimeout(100)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(length = 12.34)
|> line(end = [-12.34, 12.34])
`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
await expect
.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn(XZ)
profile001 = startProfileAt([12.34, -12.34], sketch001)
|> xLine(length = 12.34)
|> line(end = [-12.34, 12.34])
@ -237,21 +244,22 @@ profile001 = startProfileAt([12.34, -12.34], sketch001)
`)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
}
)
})
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
}
)
}
)

View File

@ -1024,6 +1024,10 @@ export function testsInputPath(fileName: string): string {
return path.join('rust', 'kcl-lib', 'tests', 'inputs', fileName)
}
export function kclSamplesPath(fileName: string): string {
return path.join('public', 'kcl-samples', fileName)
}
export async function doAndWaitForImageDiff(
page: Page,
fn: () => Promise<unknown>,

View File

@ -4,7 +4,7 @@ import { uuidv4 } from '@src/lib/utils'
import { getUtils, orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
test.describe('Testing Camera Movement', () => {
test('Can move camera reliably', async ({
page,
context,

View File

@ -10,7 +10,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
test.describe('Testing constraints', () => {
test('Can constrain line length', async ({ page, homePage }) => {
await page.addInitScript(async () => {
localStorage.setItem(

View File

@ -4,7 +4,7 @@ import { TEST_CODE_GIZMO } from '@e2e/playwright/storageStates'
import { getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing Gizmo', { tag: ['@skipWin'] }, () => {
test.describe('Testing Gizmo', () => {
const cases = [
{
testDescription: 'top view',

View File

@ -11,7 +11,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
test.describe('Testing segment overlays', () => {
test('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
// TODO: fix this test on mac after the electron migration
test.fixme(orRunWhenFullSuiteEnabled())

View File

@ -9,7 +9,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test.describe('Testing selections', () => {
test.setTimeout(90_000)
test('Selections work on fresh and edited sketch', async ({
page,
@ -39,12 +39,12 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
})
const emptySpaceHover = () =>
test.step('Hover over empty space', async () => {
await page.mouse.move(700, 143, { steps: 5 })
await page.mouse.move(1000, 143, { steps: 5 })
await expect(page.locator('.hover-highlight')).not.toBeVisible()
})
const emptySpaceClick = () =>
test.step(`Click in empty space`, async () => {
await page.mouse.click(700, 143)
await page.mouse.click(1000, 143)
await expect(page.locator('.cm-line').last()).toHaveClass(
/cm-activeLine/
)

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page)
@ -436,93 +436,92 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
})
// This will be fine once greg makes prompt at top of file deterministic
test(
'can do many at once and get many prompts back, and interact with many',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
test.fixme(orRunWhenFullSuiteEnabled())
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
test('can do many at once and get many prompts back, and interact with many', async ({
page,
homePage,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
// Let this test run longer since we've seen it timeout.
test.setTimeout(180_000)
const u = await getUtils(page)
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBar(page, 'a 2x4 lego')
await sendPromptFromCommandBar(page, 'a 2x4 lego')
await sendPromptFromCommandBar(page, 'a 2x8 lego')
await sendPromptFromCommandBar(page, 'a 2x8 lego')
await sendPromptFromCommandBar(page, 'a 2x10 lego')
await sendPromptFromCommandBar(page, 'a 2x10 lego')
// Find the toast.
// Look out for the toast message
const submittingToastMessage = page.getByText(
`Submitting to Text-to-CAD API...`
)
await expect(submittingToastMessage.first()).toBeVisible()
// Find the toast.
// Look out for the toast message
const submittingToastMessage = page.getByText(
`Submitting to Text-to-CAD API...`
)
await expect(submittingToastMessage.first()).toBeVisible()
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage.first()).toBeVisible({
timeout: 10_000,
})
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage.first()).toBeVisible({
timeout: 10_000,
})
const successToastMessage = page.getByText(`Text-to-CAD successful`)
// We should have three success toasts.
await expect(successToastMessage).toHaveCount(3, { timeout: 25_000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`)
// We should have three success toasts.
await expect(successToastMessage).toHaveCount(3, { timeout: 25_000 })
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
await expect(page.getByText(`a 2x10 lego`)).toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
await expect(page.getByText(`a 2x10 lego`)).toBeVisible()
// Ensure if you reject one, the others stay.
const rejectButton = page.getByRole('button', { name: 'Reject' })
await expect(rejectButton.first()).toBeVisible()
// Click the reject button on the first toast.
await rejectButton.first().click()
// Ensure if you reject one, the others stay.
const rejectButton = page.getByRole('button', { name: 'Reject' })
await expect(rejectButton.first()).toBeVisible()
// Click the reject button on the first toast.
await rejectButton.first().click()
// The first toast should disappear, but not the others.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// The first toast should disappear, but not the others.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for one of the models remaining.
const copyToClipboardButton = page.getByRole('button', {
name: 'Accept',
})
await expect(copyToClipboardButton.first()).toBeVisible()
// Click the button.
await copyToClipboardButton.first().click()
// Ensure you can copy the code for one of the models remaining.
const copyToClipboardButton = page.getByRole('button', {
name: 'Accept',
})
await expect(copyToClipboardButton.first()).toBeVisible()
// Click the button.
await copyToClipboardButton.first().click()
// Do NOT do AI tests like this: "Expect the code to be pasted."
// Reason: AI tests are NONDETERMINISTIC. Thus we need to be as most
// general as we can for the assertion.
// We can use Kolmogorov complexity as a measurement of the
// "probably most minimal version of this program" to have a lower
// bound to work with. It is completely by feel because there are
// no proofs that any program is its smallest self.
const code2x8 = await page.locator('.cm-content').innerText()
await expect(code2x8.length).toBeGreaterThan(249)
// Do NOT do AI tests like this: "Expect the code to be pasted."
// Reason: AI tests are NONDETERMINISTIC. Thus we need to be as most
// general as we can for the assertion.
// We can use Kolmogorov complexity as a measurement of the
// "probably most minimal version of this program" to have a lower
// bound to work with. It is completely by feel because there are
// no proofs that any program is its smallest self.
const code2x8 = await page.locator('.cm-content').innerText()
await expect(code2x8.length).toBeGreaterThan(249)
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model.
await expect(copyToClipboardButton).toBeVisible()
// Click the button.
await copyToClipboardButton.click()
// Ensure you can copy the code for the final model.
await expect(copyToClipboardButton).toBeVisible()
// Click the button.
await copyToClipboardButton.click()
// Expect the code to be pasted.
const code2x4 = await page.locator('.cm-content').innerText()
await expect(code2x4.length).toBeGreaterThan(249)
}
)
// Expect the code to be pasted.
const code2x4 = await page.locator('.cm-content').innerText()
await expect(code2x4.length).toBeGreaterThan(249)
})
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
page,

View File

@ -178,6 +178,7 @@ test('Keyboard shortcuts can be viewed through the help menu', async ({
test('First escape in tool pops you out of tool, second exits sketch mode', async ({
page,
homePage,
toolbar,
}) => {
// Wait for the app to be ready for use
const u = await getUtils(page)
@ -188,15 +189,6 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const lineButton = page.getByRole('button', {
name: 'line Line',
exact: true,
})
const arcButton = page.getByRole('button', {
name: 'arc Tangential Arc',
exact: true,
})
// Test these hotkeys perform actions when
// focus is on the canvas
await page.mouse.move(600, 250)
@ -207,8 +199,8 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await page.mouse.move(800, 300)
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toBeVisible()
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
await expect(toolbar.lineBtn).toBeVisible()
await expect(toolbar.lineBtn).toHaveAttribute('aria-pressed', 'true')
// Draw a line
await page.mouse.move(700, 200, { steps: 5 })
@ -224,10 +216,9 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
// Equip arc tool
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
await toolbar.selectTangentialArc()
// click in the same position again to continue the profile
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
@ -238,11 +229,14 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await expect(toolbar.tangentialArcBtn).toHaveAttribute(
'aria-pressed',
'false'
)
await expect
.poll(async () => {
await page.keyboard.press('l')
return lineButton.getAttribute('aria-pressed')
return toolbar.lineBtn.getAttribute('aria-pressed')
})
.toBe('true')
@ -251,8 +245,11 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
// Unequip line tool
await page.keyboard.press('Escape')
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
await expect(toolbar.lineBtn).toHaveAttribute('aria-pressed', 'false')
await expect(toolbar.tangentialArcBtn).toHaveAttribute(
'aria-pressed',
'false'
)
// Make sure we didn't pop out of sketch mode.
await expect(page.getByRole('button', { name: 'Exit Sketch' })).toBeVisible()
// Exit sketch

View File

@ -30,21 +30,68 @@ declare module '@playwright/test' {
// *for one worker*.
const electronZooInstance = new ElectronZoo()
// Track whether this is the first run for this worker process
// Mac needs more time for the first window creation
let isFirstRun = true
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed.
const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
}>({
tronApp: async ({}, use, testInfo) => {
if (process.env.PLATFORM === 'web') {
await use(undefined)
return
}
tronApp: [
async ({}, use, testInfo) => {
if (process.env.PLATFORM === 'web') {
await use(undefined)
return
}
await electronZooInstance.createInstanceIfMissing(testInfo)
await use(electronZooInstance)
await electronZooInstance.makeAvailableAgain()
},
// Create a single timeout for the entire tronApp setup process
// This will ensure tests fail faster if there's an issue with setup
// instead of waiting for the full global timeout (120s)
// First runs need more time especially on Mac for window creation
const setupTimeout = isFirstRun ? 120_000 : 30_000
let timeoutId: NodeJS.Timeout | undefined
const setupPromise = new Promise<void>((resolve, reject) => {
timeoutId = setTimeout(() => {
reject(
new Error(
`tronApp setup timed out after ${setupTimeout}ms${isFirstRun ? ' (first run)' : ' (subsequent run)'}`
)
)
}, setupTimeout)
// Execute the async setup in a separate function
const doSetup = async () => {
try {
await electronZooInstance.createInstanceIfMissing(testInfo)
resolve()
} catch (error) {
reject(error)
}
}
// Start the setup process
void doSetup()
})
try {
await setupPromise
if (timeoutId) clearTimeout(timeoutId)
// First run is complete at this point
isFirstRun = false
await use(electronZooInstance)
await electronZooInstance.makeAvailableAgain()
} catch (error) {
if (timeoutId) clearTimeout(timeoutId)
throw error
}
},
{ timeout: 120_000 }, // Keep the global timeout as fallback
],
})
const test = playwrightTestFnWithFixtures_.extend<Fixtures>(

1
interface.d.ts vendored
View File

@ -20,6 +20,7 @@ export interface IElectronAPI {
open: typeof dialog.showOpenDialog
save: typeof dialog.showSaveDialog
openExternal: typeof shell.openExternal
openInNewWindow: (name: string) => void
takeElectronWindowScreenshot: ({
width,
height,

View File

@ -3,14 +3,13 @@
> dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx
• Circular Dependencies
01) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts
02) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
03) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
04) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
05) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/engineStreamMachine.ts
06) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/settingsMachine.ts
07) src/machines/appMachine.ts -> src/machines/settingsMachine.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts
08) src/lib/singletons.ts -> src/lang/codeManager.ts
09) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
10) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
11) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx
1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts
2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
3) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
5) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
6) src/lib/singletons.ts -> src/lang/codeManager.ts
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
8) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
9) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
10) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx

View File

@ -76,6 +76,7 @@
"yargs": "^17.7.2"
},
"scripts": {
"prepare": "husky",
"install:rust": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none && source \"$HOME/.cargo/env\" && (cd rust && (rustup show active-toolchain || rustup toolchain install))",
"install:rust:windows": "winget install Microsoft.VisualStudio.2022.Community --silent --override \"--wait --quiet --add ProductLang En-us --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended\" && winget install Rustlang.Rustup",
"install:wasm-pack:sh": ". $HOME/.cargo/env && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f",
@ -114,6 +115,7 @@
"circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx",
"circular-deps:overwrite": "npm run circular-deps | sed '$d' | grep -v '^npm run' > known-circular.txt",
"circular-deps:diff": "./scripts/diff-circular-deps.sh",
"circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -125,7 +127,7 @@
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",
"tron:start": "electron-forge start",
"chrome:test": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --project='Google Chrome' --grep-invert='@snapshot'",
"chrome:test": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --project='Google Chrome' --grep-invert=@snapshot",
"tronb:vite:dev": "vite build -c vite.main.config.ts -m development && vite build -c vite.preload.config.ts -m development && vite build -c vite.renderer.config.ts -m development",
"tronb:vite:prod": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
"tronb:package:dev": "npm run tronb:vite:dev && electron-builder --config electron-builder.yml",
@ -135,15 +137,15 @@
"test:snapshots": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --grep=@snapshot --trace=on --shard=1/1",
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
"test:playwright:electron:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
"test:playwright:electron:windows:local": "npm run tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:ubuntu:engine:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot|@skipLocalEngine'",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep=@windows --quiet",
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep=@macos --quiet",
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot --quiet",
"test:playwright:electron:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:windows:local": "npm run tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:macos:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:ubuntu:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:ubuntu:engine:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot|@skipLocalEngine",
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "npm run simpleserver:bg && npm run test:unit:kcl-samples; kill-port 3000"
},

View File

@ -126,7 +126,7 @@ fn armRestProfile(plane, offset) {
export fn armRest(plane, offset) {
path = armRestPath( offsetPlane(plane, offset = offset))
profile = armRestProfile( offsetPlane(-XZ, offset = 20), offset)
profile = armRestProfile( offsetPlane(-XZ, offset = 20), -offset)
sweep(profile, path = path)
return 0
}

View File

@ -17,8 +17,8 @@ import armRest from "bench-parts.kcl"
// Create the dividers, these hold the seat and back slats
divider(YZ)
divider(offsetPlane(-YZ, offset = benchLength / 2))
divider(offsetPlane(YZ, offset = benchLength / 2))
divider(offsetPlane(YZ, offset = -benchLength / 2))
// Create the connectors to join the dividers
connector(offsetPlane(YZ, offset = -benchLength / 2), benchLength)
@ -30,5 +30,5 @@ seatSlats(offsetPlane(YZ, offset = -benchLength / 2 - (dividerThickness / 2)), b
backSlats(offsetPlane(YZ, offset = -benchLength / 2 - (dividerThickness / 2)), benchLength + dividerThickness)
// Create the arm rests
armRest(-YZ, benchLength / 2)
armRest(-YZ, -benchLength / 2)
armRest(YZ, benchLength / 2)
armRest(YZ, -benchLength / 2)

View File

@ -1,88 +1,79 @@
// Hollow Dodecahedron
// A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards.
// Dodecahedron
// A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the a dodecahedron with a series of intersects.
// Set units
@settings(defaultLengthUnit = in)
// Input parameters
// circumscribed radius
circR = 25
// Define the dihedral angle for a regular dodecahedron
dihedral = 116.565
// Calculated parameters
// Thickness of the dodecahedron
wallThickness = circR * 0.2
// Angle between faces in radians
dihedral = acos(-(sqrt(5) / 5))
// Inscribed radius
inscR = circR / 15 * sqrt(75 + 30 * sqrt(5))
// Pentagon edge length
edgeL = 4 * circR / (sqrt(3) * (1 + sqrt(5)))
// Pentagon radius
pentR = edgeL / 2 / sin(toRadians(36))
// Define a plane for the bottom angled face
plane = {
origin = [
-inscR * cos(toRadians(toDegrees(dihedral) - 90)),
0,
inscR - (inscR * sin(toRadians(toDegrees(dihedral) - 90)))
],
xAxis = [cos(dihedral), 0.0, sin(dihedral)],
yAxis = [0, 1, 0],
zAxis = [sin(dihedral), 0, -cos(dihedral)]
// Create a face template function that makes a large thin cube
fn createFaceTemplate(dither) {
baseSketch = startSketchOn(XY)
|> startProfileAt([-1000 - dither, -1000 - dither], %)
|> line(endAbsolute = [1000 + dither, -1000 - dither])
|> line(endAbsolute = [1000 + dither, 1000 + dither])
|> line(endAbsolute = [-1000 - dither, 1000 + dither])
|> close()
extruded = extrude(baseSketch, length = 1000 + dither + 1000)
return extruded
|> translate(x = 0, y = 0, z = -260 - dither)
}
// Create a regular pentagon inscribed in a circle of radius pentR
bottomFace = startSketchOn(XY)
|> polygon(
radius = pentR,
numSides = 5,
center = [0, 0],
inscribed = true,
)
// Define the rotations array with [pitch, roll, yaw, dither] for each face
faceRotations = [
[0, 0, 0, 0],
// face1 - reference face
[dihedral, 0, 0, 0.1],
// face2
[dihedral, 0, 72, 0.2],
// face3
[dihedral, 0, 144, 0.3],
// face4
[dihedral, 0, 216, 0.4],
// face5
[dihedral, 0, 288, 0.5],
// face6
[180, 0, 0, 0.6],
// face7
[180 - dihedral, 0, 36, 0.7],
// face8
[180 - dihedral, 0, 108, 0.8],
// face9
[180 - dihedral, 0, 180, 0.9],
// face10
[180 - dihedral, 0, 252, 0.11],
// face11
[180 - dihedral, 0, 324, 0.12],
// face12
]
bottomSideFace = startSketchOn(plane)
|> polygon(
radius = pentR,
numSides = 5,
center = [0, 0],
inscribed = true,
)
// Create faces by mapping over the rotations array
dodecFaces = map(faceRotations, fn(rotation) {
return createFaceTemplate(rotation[3])
|> rotate(
pitch = rotation[0],
roll = rotation[1],
yaw = rotation[2],
global = true,
)
})
// Extrude the faces in each plane
bottom = extrude(bottomFace, length = wallThickness)
bottomSide = extrude(bottomSideFace, length = wallThickness)
fn calculateArrayLength(arr) {
return reduce(arr, 0, fn(item, accumulator) {
return accumulator + 1
})
}
// Pattern the sides so we have a full dodecahedron
bottomBowl = patternCircular3d(
bottomSide,
instances = 5,
axis = [0, 0, 1],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
fn createIntersection(solids) {
fn reduceIntersect(previous, current) {
return intersect([previous, current])
}
lastIndex = calculateArrayLength(solids) - 1
lastSolid = solids[lastIndex]
remainingSolids = pop(solids)
return reduce(remainingSolids, lastSolid, reduceIntersect)
}
// Pattern the bottom to create the top face
patternCircular3d(
bottom,
instances = 2,
axis = [0, 1, 0],
center = [0, 0, inscR],
arcDegrees = 360,
rotateDuplicates = true,
)
// Pattern the bottom angled faces to create the top
patternCircular3d(
bottomBowl,
instances = 2,
axis = [0, 1, 0],
center = [0, 0, inscR],
arcDegrees = 360,
rotateDuplicates = true,
)
// Apply intersection to all faces
createIntersection(dodecFaces)

View File

@ -202,19 +202,17 @@ plane000 = {
height + binHeight * countBinHeight
],
xAxis = [0.0, 1.0, 0.0],
yAxis = [0.0, 0.0, 1.0],
zAxis = [1.0, 0.0, 0.0]
yAxis = [0.0, 0.0, 1.0]
}
plane001 = {
origin = [
0.0,
cornerRadius,
countBinLength * (binLength + 2 * binTol) - cornerRadius,
height + binHeight * countBinHeight
],
xAxis = [1.0, 0.0, 0.0],
yAxis = [0.0, 0.0, 1.0],
zAxis = [0.0, 1.0, 0.0]
yAxis = [0.0, 0.0, 1.0]
}
plane002 = {
@ -224,8 +222,7 @@ plane002 = {
height + binHeight * countBinHeight
],
xAxis = [0.0, 1.0, 0.0],
yAxis = [0.0, 0.0, 1.0],
zAxis = [1.0, 0.0, 0.0]
yAxis = [0.0, 0.0, 1.0]
}
// Extrude a single side of the lip of the bin

View File

@ -66,8 +66,8 @@
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
"multipleFiles": false,
"title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
"title": "Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the a dodecahedron with a series of intersects."
},
{
"file": "main.kcl",

View File

@ -4,10 +4,10 @@
@settings(defaultLengthUnit = in)
// Axis Angles
export axisJ4 = 25
export axisJ3 = 60
export axisJ2 = 110
export axisJ1 = 80
export axisJ4 = 25deg
export axisJ3 = 60deg
export axisJ2 = 110deg
export axisJ1 = 80deg
// Robot Arm Base
export basePlateRadius = 5
@ -30,29 +30,26 @@ export axisJ3CArmThickness = 2.5
export plane001 = {
origin = [0.0, 0.0, baseHeight - 1.5 + 0.1],
xAxis = [1.0, 0.0, 0.0],
yAxis = [0.0, 1.0, 0.0],
zAxis = [0.0, 0.0, 1.0]
yAxis = [0.0, 1.0, 0.0]
}
export plane002 = {
origin = [0.0, 0.0, 0.0],
xAxis = [
sin(toRadians(axisJ1)),
cos(toRadians(axisJ1)),
sin(axisJ1): number(in),
cos(axisJ1): number(in),
0.0
],
yAxis = [0.0, 0.0, 1.0],
zAxis = [1.0, 0.0, 0.0]
yAxis = [0.0, 0.0, 1.0]
}
// Define Plane to Move J2 Axis Robot Arm
export plane003 = {
origin = [-0.1, 0.0, 0.0],
xAxis = [
sin(toRadians(axisJ1)),
cos(toRadians(axisJ1)),
sin(axisJ1): number(in),
cos(axisJ1): number(in),
0.0
],
yAxis = [0.0, 0.0, 1.0],
zAxis = [1.0, 0.0, 0.0]
yAxis = [0.0, 0.0, 1.0]
}

View File

@ -62,8 +62,7 @@ customPlane = {
z = 0
},
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = -1, z = 0 }
yAxis = { x = 0, y = 0, z = 1 }
}
sketch003 = startSketchOn(customPlane)
|> startProfileAt([0, 0], %)
@ -98,19 +97,18 @@ customPlane2 = {
y = 0,
z = 0
},
xAxis = { x = 0, y = -1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 }
}
sketch005 = startSketchOn(customPlane2)
|> startProfileAt([0, 0], %)
|> yLine(endAbsolute = height)
|> xLine(endAbsolute = wallsWidth)
|> xLine(endAbsolute = -wallsWidth)
|> tangentialArc(endAbsolute = [
(frontLength - wallsWidth) / 2 + wallsWidth,
-1 * ((frontLength - wallsWidth) / 2 + wallsWidth),
height - ((height - exitHeight) / 2)
])
|> tangentialArc(endAbsolute = [frontLength, exitHeight])
|> tangentialArc(endAbsolute = [-frontLength, exitHeight])
|> yLine(endAbsolute = 0, tag = $seg03)
|> close()
|> extrude(length = wallThickness)
@ -138,8 +136,7 @@ customPlane3 = {
z = wallThickness
},
xAxis = { x = 0, y = -1, z = 0 },
yAxis = { x = 1, y = 0, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
yAxis = { x = 1, y = 0, z = 0 }
}
sketch008 = startSketchOn(customPlane3)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -6,7 +6,7 @@ uses-engine = { max-threads = 4 }
after-engine = { max-threads = 12 }
[profile.default]
slow-timeout = { period = "90s", terminate-after = 1 }
slow-timeout = { period = "180s", terminate-after = 1 }
[profile.ci]
slow-timeout = { period = "50s", terminate-after = 5 }

View File

@ -6,11 +6,12 @@
mod tests;
mod unbox;
use std::collections::HashMap;
use std::{collections::HashMap, fs};
use convert_case::Casing;
use inflector::{cases::camelcase::to_camel_case, Inflector};
use once_cell::sync::Lazy;
use proc_macro2::Span;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use regex::Regex;
use serde::Deserialize;
@ -21,6 +22,16 @@ use syn::{
};
use unbox::unbox;
#[proc_macro_attribute]
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
#[proc_macro_attribute]
pub fn for_each_std_mod(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_for_each_std_mod(item.into()).into()
}
/// Describes an argument of a stdlib function.
#[derive(Deserialize, Debug)]
struct ArgMetadata {
@ -73,11 +84,6 @@ struct StdlibMetadata {
args: HashMap<String, ArgMetadata>,
}
#[proc_macro_attribute]
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
fn do_stdlib(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
@ -86,6 +92,31 @@ fn do_stdlib(
do_stdlib_inner(metadata, attr, item)
}
fn do_for_each_std_mod(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let item: syn::ItemFn = syn::parse2(item.clone()).unwrap();
let mut result = proc_macro2::TokenStream::new();
for name in fs::read_dir("kcl-lib/std").unwrap().filter_map(|e| {
let e = e.unwrap();
let filename = e.file_name();
filename.to_str().unwrap().strip_suffix(".kcl").map(str::to_owned)
}) {
let mut item = item.clone();
item.sig.ident = syn::Ident::new(&format!("{}_{}", item.sig.ident, name), Span::call_site());
let stmts = &item.block.stmts;
//let name = format!("\"{name}\"");
let block = quote! {
{
const STD_MOD_NAME: &str = #name;
#(#stmts)*
}
};
item.block = Box::new(syn::parse2(block).unwrap());
result.extend(Some(item.into_token_stream()));
}
result
}
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
@ -671,6 +702,7 @@ fn normalize_comment_string(s: String) -> Vec<String> {
/// Represent an item without concern for its body which may (or may not)
/// contain syntax errors.
#[derive(Clone)]
struct ItemFnForSignature {
pub attrs: Vec<Attribute>,
pub vis: Visibility,

View File

@ -1,9 +0,0 @@
const part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line(end = [1, 3.82], tag = $seg01)
|> angled(
angle = -angleToMatchLengthX(seg01, 3, %),
endAbsoluteX = 3,
)
|> close()
|> extrude(length = 10)

View File

@ -1,9 +0,0 @@
const part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line(end = [1, 3.82], tag = $seg01)
|> angledLine(
angle = -angleToMatchLengthY(seg01, 3, %),
endAbsoluteX = 3,
)
|> close()
|> extrude(length = 10)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -61,8 +61,10 @@ impl CollectionVisitor {
format!("std::{}::", self.name)
};
let mut dd = match var.kind {
VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual_name, preferred_prefix)),
VariableKind::Const => DocData::Const(ConstData::from_ast(var, qual_name, preferred_prefix)),
VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual_name, preferred_prefix, name)),
VariableKind::Const => {
DocData::Const(ConstData::from_ast(var, qual_name, preferred_prefix, name))
}
};
dd.with_meta(&var.outer_attrs);
@ -79,7 +81,7 @@ impl CollectionVisitor {
} else {
format!("std::{}::", self.name)
};
let mut dd = DocData::Ty(TyData::from_ast(ty, qual_name, preferred_prefix));
let mut dd = DocData::Ty(TyData::from_ast(ty, qual_name, preferred_prefix, name));
dd.with_meta(&ty.outer_attrs);
for a in &ty.outer_attrs {
@ -114,6 +116,16 @@ impl DocData {
}
}
/// The name of the module in which the item is declared, e.g., `sketch`
#[allow(dead_code)]
pub fn module_name(&self) -> &str {
match self {
DocData::Fn(f) => &f.module_name,
DocData::Const(c) => &c.module_name,
DocData::Ty(t) => &t.module_name,
}
}
#[allow(dead_code)]
pub fn file_name(&self) -> String {
match self {
@ -132,6 +144,7 @@ impl DocData {
}
}
/// The path to the module through which the item is accessed, e.g., `std::sketch`
#[allow(dead_code)]
pub fn mod_name(&self) -> String {
let q = match self {
@ -217,6 +230,8 @@ pub struct ConstData {
/// Code examples.
/// These are tested and we know they compile and execute.
pub examples: Vec<(String, ExampleProperties)>,
pub module_name: String,
}
impl ConstData {
@ -224,6 +239,7 @@ impl ConstData {
var: &crate::parsing::ast::types::VariableDeclaration,
mut qual_name: String,
preferred_prefix: &str,
module_name: &str,
) -> Self {
assert_eq!(var.kind, crate::parsing::ast::types::VariableKind::Const);
@ -263,6 +279,7 @@ impl ConstData {
summary: None,
description: None,
examples: Vec::new(),
module_name: module_name.to_owned(),
}
}
@ -334,6 +351,8 @@ pub struct FnData {
pub examples: Vec<(String, ExampleProperties)>,
#[allow(dead_code)]
pub referenced_types: Vec<String>,
pub module_name: String,
}
impl FnData {
@ -341,6 +360,7 @@ impl FnData {
var: &crate::parsing::ast::types::VariableDeclaration,
mut qual_name: String,
preferred_prefix: &str,
module_name: &str,
) -> Self {
assert_eq!(var.kind, crate::parsing::ast::types::VariableKind::Fn);
let crate::parsing::ast::types::Expr::FunctionExpression(expr) = &var.declaration.init else {
@ -375,6 +395,7 @@ impl FnData {
description: None,
examples: Vec::new(),
referenced_types: referenced_types.into_iter().collect(),
module_name: module_name.to_owned(),
}
}
@ -654,6 +675,8 @@ pub struct TyData {
pub examples: Vec<(String, ExampleProperties)>,
#[allow(dead_code)]
pub referenced_types: Vec<String>,
pub module_name: String,
}
impl TyData {
@ -661,6 +684,7 @@ impl TyData {
ty: &crate::parsing::ast::types::TypeDeclaration,
mut qual_name: String,
preferred_prefix: &str,
module_name: &str,
) -> Self {
let name = ty.name.name.clone();
qual_name.push_str(&name);
@ -684,6 +708,7 @@ impl TyData {
description: None,
examples: Vec::new(),
referenced_types: referenced_types.into_iter().collect(),
module_name: module_name.to_owned(),
}
}
@ -1009,6 +1034,8 @@ fn collect_type_names_from_primitive(ty: &PrimitiveType) -> String {
#[cfg(test)]
mod test {
use kcl_derive_docs::for_each_std_mod;
use super::*;
#[test]
@ -1047,18 +1074,28 @@ mod test {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn test_examples() -> miette::Result<()> {
#[for_each_std_mod]
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_examples() {
let std = walk_prelude();
let mut errs = Vec::new();
for d in std {
if d.module_name() != STD_MOD_NAME {
continue;
}
for (i, eg) in d.examples().enumerate() {
let result = match crate::test_server::execute_and_snapshot(eg, None).await {
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{i}", d.name()),
kcl_source: eg.to_string(),
}));
errs.push(
miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{i}", d.name()),
kcl_source: eg.to_string(),
})
.to_string(),
);
continue;
}
Err(other_err) => panic!("{}", other_err),
Ok(img) => img,
@ -1071,6 +1108,8 @@ mod test {
}
}
Ok(())
if !errs.is_empty() {
panic!("{}", errs.join("\n\n"));
}
}
}

View File

@ -129,6 +129,8 @@ impl StdLibFnArg {
};
if (self.type_ == "Sketch"
|| self.type_ == "[Sketch]"
|| self.type_ == "Geometry"
|| self.type_ == "GeometryWithImportedGeometry"
|| self.type_ == "Solid"
|| self.type_ == "[Solid]"
|| self.type_ == "SketchSurface"
@ -502,6 +504,8 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
fn to_autocomplete_snippet(&self) -> Result<String> {
if self.name() == "loft" {
return Ok("loft([${0:sketch000}, ${1:sketch001}])".to_string());
} else if self.name() == "clone" {
return Ok("clone(${0:part001})".to_string());
} else if self.name() == "union" {
return Ok("union([${0:extrude001}, ${1:extrude002}])".to_string());
} else if self.name() == "subtract" {
@ -1089,6 +1093,14 @@ mod tests {
);
}
#[test]
#[allow(clippy::literal_string_with_formatting_args)]
fn get_autocomplete_snippet_clone() {
let clone_fn: Box<dyn StdLibFn> = Box::new(crate::std::clone::Clone);
let snippet = clone_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"clone(${0:part001})"#);
}
// We want to test the snippets we compile at lsp start.
#[test]
fn get_all_stdlib_autocomplete_snippets() {

View File

@ -459,7 +459,7 @@ impl ExecutorContext {
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let format = super::import::format_from_annotations(attrs, path, source_range)?;
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None));
Ok(id)
}
ImportPath::Std { .. } => {
@ -501,7 +501,7 @@ impl ExecutorContext {
*cache = Some((val, er, items.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom) => Err(KclError::Semantic(KclErrorDetails {
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
message: "Cannot import items from foreign modules".to_owned(),
source_ranges: vec![geom.source_range],
})),
@ -546,9 +546,20 @@ impl ExecutorContext {
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(geom) => super::import::send_to_engine(geom.clone(), self)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom))),
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
ModuleRepr::Foreign(geom, cached) => {
let result = super::import::send_to_engine(geom.clone(), self)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result {
Ok(val) => {
*cached = val.clone();
Ok(val)
}
Err(e) => Err(e),
}
}
ModuleRepr::Dummy => unreachable!(),
};
@ -1142,9 +1153,15 @@ impl Node<UnaryExpression> {
}
KclValue::Plane { value } => {
let mut plane = value.clone();
plane.z_axis.x *= -1.0;
plane.z_axis.y *= -1.0;
plane.z_axis.z *= -1.0;
if plane.x_axis.x != 0.0 {
plane.x_axis.x *= -1.0;
}
if plane.x_axis.y != 0.0 {
plane.x_axis.y *= -1.0;
}
if plane.x_axis.z != 0.0 {
plane.x_axis.z *= -1.0;
}
plane.value = PlaneType::Uninit;
plane.id = exec_state.next_uuid();
@ -2637,7 +2654,6 @@ p = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}: Plane
p2 = -p
"#;
@ -2649,7 +2665,11 @@ p2 = -p
.get_from("p2", result.mem_env, SourceRange::default(), 0)
.unwrap()
{
KclValue::Plane { value } => assert_eq!(value.z_axis.z, -1.0),
KclValue::Plane { value } => {
assert_eq!(value.x_axis.x, -1.0);
assert_eq!(value.x_axis.y, 0.0);
assert_eq!(value.x_axis.z, 0.0);
}
_ => unreachable!(),
}
}

View File

@ -47,6 +47,29 @@ impl Geometry {
}
}
/// A geometry including an imported geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum GeometryWithImportedGeometry {
Sketch(Sketch),
Solid(Solid),
ImportedGeometry(Box<ImportedGeometry>),
}
impl GeometryWithImportedGeometry {
pub async fn id(&mut self, ctx: &ExecutorContext) -> Result<uuid::Uuid, KclError> {
match self {
GeometryWithImportedGeometry::Sketch(s) => Ok(s.id),
GeometryWithImportedGeometry::Solid(e) => Ok(e.id),
GeometryWithImportedGeometry::ImportedGeometry(i) => {
let id = i.id(ctx).await?;
Ok(id)
}
}
}
}
/// A set of geometry.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -262,8 +285,6 @@ pub struct Plane {
pub x_axis: Point3d,
/// What should the plane's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -296,13 +317,6 @@ impl Plane {
z: 0.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 0.0,
z: 1.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::XY,
Self {
@ -315,7 +329,7 @@ impl Plane {
},
x_axis:
Point3d {
x: 1.0,
x: -1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
@ -327,13 +341,6 @@ impl Plane {
z: 0.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 0.0,
z: -1.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegXY,
Self {
@ -358,13 +365,6 @@ impl Plane {
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: -1.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::XZ,
Self {
@ -377,7 +377,7 @@ impl Plane {
},
x_axis:
Point3d {
x: 1.0,
x: -1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
@ -389,13 +389,6 @@ impl Plane {
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 0.0,
y: 1.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegXZ,
Self {
@ -420,13 +413,6 @@ impl Plane {
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: 1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::YZ,
Self {
@ -440,7 +426,7 @@ impl Plane {
x_axis:
Point3d {
x: 0.0,
y: 1.0,
y: -1.0,
z: 0.0,
units: UnitLen::Mm,
},
@ -451,13 +437,6 @@ impl Plane {
z: 1.0,
units: UnitLen::Mm,
},
z_axis:
Point3d {
x: -1.0,
y: 0.0,
z: 0.0,
units: UnitLen::Mm,
},
..
} => return PlaneData::NegYZ,
_ => {}
@ -468,7 +447,6 @@ impl Plane {
origin: self.origin,
x_axis: self.x_axis,
y_axis: self.y_axis,
z_axis: self.z_axis,
}
}
@ -481,7 +459,6 @@ impl Plane {
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
value: PlaneType::XY,
meta: vec![],
},
@ -489,9 +466,8 @@ impl Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 0.0, -1.0, UnitLen::Mm),
value: PlaneType::XY,
meta: vec![],
},
@ -501,7 +477,6 @@ impl Plane {
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Mm),
value: PlaneType::XZ,
meta: vec![],
},
@ -511,7 +486,6 @@ impl Plane {
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
value: PlaneType::XZ,
meta: vec![],
},
@ -521,7 +495,6 @@ impl Plane {
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(1.0, 0.0, 0.0, UnitLen::Mm),
value: PlaneType::YZ,
meta: vec![],
},
@ -529,18 +502,12 @@ impl Plane {
id,
artifact_id: id.into(),
origin: Point3d::new(0.0, 0.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(0.0, 1.0, 0.0, UnitLen::Mm),
x_axis: Point3d::new(0.0, -1.0, 0.0, UnitLen::Mm),
y_axis: Point3d::new(0.0, 0.0, 1.0, UnitLen::Mm),
z_axis: Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
value: PlaneType::YZ,
meta: vec![],
},
PlaneData::Plane {
origin,
x_axis,
y_axis,
z_axis,
} => {
PlaneData::Plane { origin, x_axis, y_axis } => {
let id = exec_state.next_uuid();
Plane {
id,
@ -548,7 +515,6 @@ impl Plane {
origin,
x_axis,
y_axis,
z_axis,
value: PlaneType::Custom,
meta: vec![],
}
@ -577,8 +543,6 @@ pub struct Face {
pub x_axis: Point3d,
/// What should the face's Y axis be?
pub y_axis: Point3d,
/// The z-axis (normal).
pub z_axis: Point3d,
/// The solid the face is on.
pub solid: Box<Solid>,
pub units: UnitLen,
@ -656,7 +620,8 @@ impl Sketch {
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &self.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
let normal = plane.x_axis.cross(&plane.y_axis);
Some(normal.into())
} else {
None
},
@ -700,12 +665,6 @@ impl SketchSurface {
SketchSurface::Face(face) => face.y_axis,
}
}
pub(crate) fn z_axis(&self) -> Point3d {
match self {
SketchSurface::Plane(plane) => plane.z_axis,
SketchSurface::Face(face) => face.z_axis,
}
}
}
#[derive(Debug, Clone)]
@ -807,7 +766,10 @@ pub struct Solid {
/// Chamfers or fillets on this solid.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_cuts: Vec<EdgeCut>,
/// The units of the solid.
pub units: UnitLen,
/// Is this a sectional solid?
pub sectional: bool,
/// Metadata.
#[serde(skip)]
pub meta: Vec<Metadata>,
@ -858,6 +820,13 @@ impl EdgeCut {
}
}
pub fn set_id(&mut self, id: uuid::Uuid) {
match self {
EdgeCut::Fillet { id: ref mut i, .. } => *i = id,
EdgeCut::Chamfer { id: ref mut i, .. } => *i = id,
}
}
pub fn edge_id(&self) -> uuid::Uuid {
match self {
EdgeCut::Fillet { edge_id, .. } => *edge_id,
@ -865,6 +834,13 @@ impl EdgeCut {
}
}
pub fn set_edge_id(&mut self, id: uuid::Uuid) {
match self {
EdgeCut::Fillet { edge_id: ref mut i, .. } => *i = id,
EdgeCut::Chamfer { edge_id: ref mut i, .. } => *i = id,
}
}
pub fn tag(&self) -> Option<TagNode> {
match self {
EdgeCut::Fillet { tag, .. } => *tag.clone(),
@ -929,6 +905,27 @@ impl Point3d {
pub const fn is_zero(&self) -> bool {
self.x == 0.0 && self.y == 0.0 && self.z == 0.0
}
/// Calculate the cross product of this vector with another
pub fn cross(&self, other: &Self) -> Self {
let other = if other.units == self.units {
other
} else {
&Point3d {
x: self.units.adjust_to(other.x, self.units).0,
y: self.units.adjust_to(other.y, self.units).0,
z: self.units.adjust_to(other.z, self.units).0,
units: self.units,
}
};
Self {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
units: self.units,
}
}
}
impl From<[TyF64; 3]> for Point3d {
@ -1135,6 +1132,19 @@ pub enum Path {
/// True if the arc is counterclockwise.
ccw: bool,
},
/// An involute of a circle of start_radius ending at end_radius
CircularInvolute {
#[serde(flatten)]
base: BasePath,
/// The radius of the base circle of the involute
start_radius: f64,
/// The radius that the involute ends at
end_radius: f64,
/// Angle about which the whole involute is rotated
angle: f64,
/// If true, the path segment starts at the end radius and goes towards the start radius
reverse: bool,
},
}
/// What kind of path is this?
@ -1149,6 +1159,7 @@ enum PathType {
Horizontal,
AngledLineTo,
Arc,
CircularInvolute,
}
impl From<&Path> for PathType {
@ -1164,6 +1175,7 @@ impl From<&Path> for PathType {
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
Path::ArcThreePoint { .. } => Self::Arc,
Path::CircularInvolute { .. } => Self::CircularInvolute,
}
}
}
@ -1181,6 +1193,23 @@ impl Path {
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
Path::ArcThreePoint { base, .. } => base.geo_meta.id,
Path::CircularInvolute { base, .. } => base.geo_meta.id,
}
}
pub fn set_id(&mut self, id: uuid::Uuid) {
match self {
Path::ToPoint { base } => base.geo_meta.id = id,
Path::Horizontal { base, .. } => base.geo_meta.id = id,
Path::AngledLineTo { base, .. } => base.geo_meta.id = id,
Path::Base { base } => base.geo_meta.id = id,
Path::TangentialArcTo { base, .. } => base.geo_meta.id = id,
Path::TangentialArc { base, .. } => base.geo_meta.id = id,
Path::Circle { base, .. } => base.geo_meta.id = id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id = id,
Path::Arc { base, .. } => base.geo_meta.id = id,
Path::ArcThreePoint { base, .. } => base.geo_meta.id = id,
Path::CircularInvolute { base, .. } => base.geo_meta.id = id,
}
}
@ -1196,6 +1225,7 @@ impl Path {
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
Path::ArcThreePoint { base, .. } => base.tag.clone(),
Path::CircularInvolute { base, .. } => base.tag.clone(),
}
}
@ -1211,6 +1241,7 @@ impl Path {
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
Path::ArcThreePoint { base, .. } => base,
Path::CircularInvolute { base, .. } => base,
}
}
@ -1272,6 +1303,15 @@ impl Path {
// TODO: Call engine utils to figure this out.
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::CircularInvolute {
base: _,
start_radius,
end_radius,
..
} => {
let angle = (end_radius * end_radius - start_radius * start_radius).sqrt() / start_radius;
0.5 * start_radius * angle * angle
}
};
TyF64::new(n, self.get_base().units.into())
}
@ -1288,6 +1328,7 @@ impl Path {
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
Path::ArcThreePoint { base, .. } => Some(base),
Path::CircularInvolute { base, .. } => Some(base),
}
}
@ -1323,7 +1364,11 @@ impl Path {
radius: circle.radius,
}
}
Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Base { .. } => {
Path::CircularInvolute { .. }
| Path::ToPoint { .. }
| Path::Horizontal { .. }
| Path::AngledLineTo { .. }
| Path::Base { .. } => {
let base = self.get_base();
GetTangentialInfoFromPathsResult::PreviousPoint(base.from)
}
@ -1350,6 +1395,7 @@ pub enum ExtrudeSurface {
/// An extrude plane.
ExtrudePlane(ExtrudePlane),
ExtrudeArc(ExtrudeArc),
ExtrudeInvolute(ExtrudeInvolute),
Chamfer(ChamferSurface),
Fillet(FilletSurface),
}
@ -1410,11 +1456,26 @@ pub struct ExtrudeArc {
pub geo_meta: GeoMeta,
}
/// An extruded involute.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudeInvolute {
/// The face id for the extrude surface.
pub face_id: uuid::Uuid,
/// The tag.
pub tag: Option<Node<TagDeclarator>>,
/// Metadata.
#[serde(flatten)]
pub geo_meta: GeoMeta,
}
impl ExtrudeSurface {
pub fn get_id(&self) -> uuid::Uuid {
match self {
ExtrudeSurface::ExtrudePlane(ep) => ep.geo_meta.id,
ExtrudeSurface::ExtrudeArc(ea) => ea.geo_meta.id,
ExtrudeSurface::ExtrudeInvolute(ea) => ea.geo_meta.id,
ExtrudeSurface::Fillet(f) => f.geo_meta.id,
ExtrudeSurface::Chamfer(c) => c.geo_meta.id,
}
@ -1424,6 +1485,7 @@ impl ExtrudeSurface {
match self {
ExtrudeSurface::ExtrudePlane(ep) => ep.tag.clone(),
ExtrudeSurface::ExtrudeArc(ea) => ea.tag.clone(),
ExtrudeSurface::ExtrudeInvolute(ea) => ea.tag.clone(),
ExtrudeSurface::Fillet(f) => f.tag.clone(),
ExtrudeSurface::Chamfer(c) => c.tag.clone(),
}

View File

@ -9,8 +9,8 @@ use crate::{
execution::{
annotations::{SETTINGS, SETTINGS_UNIT_LENGTH},
types::{NumericType, PrimitiveType, RuntimeType, UnitLen},
EnvironmentRef, ExecState, Face, Helix, ImportedGeometry, MetaSettings, Metadata, Plane, Sketch, Solid,
TagIdentifier,
EnvironmentRef, ExecState, Face, Geometry, GeometryWithImportedGeometry, Helix, ImportedGeometry, MetaSettings,
Metadata, Plane, Sketch, Solid, TagIdentifier,
},
parsing::ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
@ -611,3 +611,22 @@ impl KclValue {
}
}
}
impl From<Geometry> for KclValue {
fn from(value: Geometry) -> Self {
match value {
Geometry::Sketch(x) => Self::Sketch { value: Box::new(x) },
Geometry::Solid(x) => Self::Solid { value: Box::new(x) },
}
}
}
impl From<GeometryWithImportedGeometry> for KclValue {
fn from(value: GeometryWithImportedGeometry) -> Self {
match value {
GeometryWithImportedGeometry::Sketch(x) => Self::Sketch { value: Box::new(x) },
GeometryWithImportedGeometry::Solid(x) => Self::Solid { value: Box::new(x) },
GeometryWithImportedGeometry::ImportedGeometry(x) => Self::ImportedGeometry(*x),
}
}
}

View File

@ -752,26 +752,31 @@ impl ExecutorContext {
let mut universe = std::collections::HashMap::new();
let default_planes = self.engine.get_default_planes().read().await.clone();
crate::walk::import_universe(self, &program.ast, &mut universe, exec_state)
.await
.map_err(|err| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
crate::walk::import_universe(
self,
&ModuleRepr::Kcl(program.ast.clone(), None),
&mut universe,
exec_state,
)
.await
.map_err(|err| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.global.operations.clone(),
exec_state.global.artifact_commands.clone(),
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?;
KclErrorWithOutputs::new(
err,
exec_state.global.operations.clone(),
exec_state.global.artifact_commands.clone(),
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?;
for modules in crate::walk::import_graph(&universe, self)
.map_err(|err| {
@ -799,16 +804,12 @@ impl ExecutorContext {
#[allow(clippy::type_complexity)]
let (results_tx, mut results_rx): (
tokio::sync::mpsc::Sender<(
ModuleId,
ModulePath,
Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError>,
)>,
tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
tokio::sync::mpsc::Receiver<_>,
) = tokio::sync::mpsc::channel(1);
for module in modules {
let Some((import_stmt, module_id, module_path, program)) = universe.get(&module) else {
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(KclErrorDetails {
message: format!("Module {module} not found in universe"),
source_ranges: Default::default(),
@ -816,12 +817,41 @@ impl ExecutorContext {
};
let module_id = *module_id;
let module_path = module_path.clone();
let program = program.clone();
let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone();
let results_tx = results_tx.clone();
let source_range = SourceRange::from(import_stmt);
let exec_module = async |exec_ctxt: &ExecutorContext,
repr: &ModuleRepr,
module_id: ModuleId,
module_path: &ModulePath,
exec_state: &mut ExecState,
source_range: SourceRange|
-> Result<ModuleRepr, KclError> {
match repr {
ModuleRepr::Kcl(program, _) => {
let result = exec_ctxt
.exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
.await;
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
}
ModuleRepr::Foreign(geom, _) => {
let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
}
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails {
message: format!("Module {module_path} not found in universe"),
source_ranges: vec![source_range],
})),
}
};
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
@ -829,16 +859,15 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
)
.await;
results_tx
.send((module_id, module_path, result))
@ -852,16 +881,15 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
)
.await;
results_tx
.send((module_id, module_path, result))
@ -875,13 +903,24 @@ impl ExecutorContext {
while let Some((module_id, _, result)) = results_rx.recv().await {
match result {
Ok((val, session_data, variables)) => {
Ok(new_repr) => {
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
let ModuleRepr::Kcl(_, cache) = &mut repr else {
continue;
};
*cache = Some((val, session_data, variables));
match &mut repr {
ModuleRepr::Kcl(_, cache) => {
let ModuleRepr::Kcl(_, session_data) = new_repr else {
unreachable!();
};
*cache = session_data;
}
ModuleRepr::Foreign(_, cache) => {
let ModuleRepr::Foreign(_, session_data) = new_repr else {
unreachable!();
};
*cache = session_data;
}
ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
}
exec_state.global.module_infos[&module_id].restore_repr(repr);
}
@ -1277,7 +1316,7 @@ const part001 = startSketchOn(XY)
|> line(end = [3, 4], tag = $seg01)
|> line(end = [
min(segLen(seg01), myVar),
-legLen(segLen(seg01), myVar)
-legLen(hypotenuse = segLen(seg01), leg = myVar)
])
"#;
@ -1292,7 +1331,7 @@ const part001 = startSketchOn(XY)
|> line(end = [3, 4], tag = $seg01)
|> line(end = [
min(segLen(seg01), myVar),
legLen(segLen(seg01), myVar)
legLen(hypotenuse = segLen(seg01), leg = myVar)
])
"#;
@ -1684,7 +1723,7 @@ let shape = layer() |> patternTransform(instances = 10, transform = transform)
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
let ast = r#"const myVar = 2 + min(100, -1 + legLen(hypotenuse = 5, leg = 3))"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
5.0,

View File

@ -28,6 +28,10 @@ pub enum RuntimeType {
}
impl RuntimeType {
pub fn edge() -> Self {
RuntimeType::Primitive(PrimitiveType::Edge)
}
pub fn sketch() -> Self {
RuntimeType::Primitive(PrimitiveType::Sketch)
}
@ -1043,10 +1047,6 @@ impl KclValue {
.get("yAxis")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let z_axis = value
.get("zAxis")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let id = exec_state.mod_local.id_generator.next_uuid();
let plane = Plane {
@ -1055,7 +1055,6 @@ impl KclValue {
origin,
x_axis,
y_axis,
z_axis,
value: super::PlaneType::Uninit,
meta: meta.clone(),
};
@ -2120,4 +2119,73 @@ d = cos(30)
assert_value_and_type("c", &result, 1.0, NumericType::count());
assert_value_and_type("d", &result, 0.0, NumericType::count());
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_nested_array() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let mixed1 = KclValue::MixedArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::Number {
value: 1.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 2.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::Number {
value: 3.0,
ty: NumericType::count(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::count())),
},
],
meta: Vec::new(),
};
// Principal types
let tym1 = RuntimeType::Array(
Box::new(RuntimeType::Primitive(PrimitiveType::Number(NumericType::count()))),
ArrayLen::NonEmpty,
);
let result = KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::Number {
value: 1.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::Number {
value: 2.0,
ty: NumericType::count(),
meta: Vec::new(),
},
KclValue::Number {
value: 3.0,
ty: NumericType::count(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::count())),
};
assert_coerce_results(&mixed1, &tym1, &result, &mut exec_state);
}
}

View File

@ -43,5 +43,5 @@ async fn main() {
.await
.unwrap();
let mut exec_state = ExecState::new(&ctx);
ctx.run(&program, &mut exec_state).await.unwrap();
ctx.run(&program, &mut exec_state).await.map_err(|e| e.error).unwrap();
}

View File

@ -124,7 +124,7 @@ pub enum ModuleRepr {
Root,
// AST, memory, exported names
Kcl(Node<Program>, Option<(Option<KclValue>, EnvironmentRef, Vec<String>)>),
Foreign(PreImportedGeometry),
Foreign(PreImportedGeometry, Option<KclValue>),
Dummy,
}

View File

@ -1354,48 +1354,6 @@ mod tangential_arc {
super::execute(TEST_NAME, true).await
}
}
mod big_number_angle_to_match_length_x {
const TEST_NAME: &str = "big_number_angle_to_match_length_x";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod big_number_angle_to_match_length_y {
const TEST_NAME: &str = "big_number_angle_to_match_length_y";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod sketch_on_face_circle_tagged {
const TEST_NAME: &str = "sketch_on_face_circle_tagged";
@ -2621,3 +2579,46 @@ mod loop_tag {
super::execute(TEST_NAME, true).await
}
}
mod multiple_foreign_imports_all_render {
const TEST_NAME: &str = "multiple-foreign-imports-all-render";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod involute_fail {
const TEST_NAME: &str = "involute_fail";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -277,11 +277,11 @@ pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclVal
/// import "tests/inputs/cube.sldprt" as cube
///
/// cube
/// // |> appearance(
/// // color = "#ff0000",
/// // metalness = 50,
/// // roughness = 50
/// // )
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "appearance",

View File

@ -615,22 +615,6 @@ impl Args {
Ok(numbers)
}
pub(crate) fn get_hypotenuse_leg(&self) -> Result<(f64, f64, NumericType), KclError> {
let numbers = self.get_number_array_with_types()?;
if numbers.len() != 2 {
return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a number array of length 2, found `{:?}`", numbers),
source_ranges: vec![self.source_range],
}));
}
let mut numbers = numbers.into_iter();
let a = numbers.next().unwrap();
let b = numbers.next().unwrap();
Ok(NumericType::combine_eq_coerce(a, b))
}
pub(crate) fn get_sketches(&self, exec_state: &mut ExecState) -> Result<(Vec<Sketch>, Sketch), KclError> {
let Some(arg0) = self.args.first() else {
return Err(KclError::Semantic(KclErrorDetails {
@ -675,21 +659,10 @@ impl Args {
Ok((sketches, sketch))
}
pub(crate) fn get_data<'a, T>(&'a self) -> Result<T, KclError>
where
T: FromArgs<'a>,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_sketch_surface(&self) -> Result<([TyF64; 2], SketchSurface, Option<TagNode>), KclError> {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_tag_to_number_sketch(&self) -> Result<(TagIdentifier, TyF64, Sketch), KclError> {
FromArgs::from_args(self, 0)
}
pub(crate) async fn get_adjacent_face_to_tag(
&self,
exec_state: &mut ExecState,
@ -740,6 +713,17 @@ impl Args {
None
}
}
ExtrudeSurface::ExtrudeInvolute(extrude_involute) => {
if let Some(involute_tag) = &extrude_involute.tag {
if involute_tag.name == tag.value {
Some(Ok(extrude_involute.face_id))
} else {
None
}
} else {
None
}
}
ExtrudeSurface::Chamfer(chamfer) => {
if let Some(chamfer_tag) = &chamfer.tag {
if chamfer_tag.name == tag.value {
@ -1028,6 +1012,27 @@ impl<'a> FromKclValue<'a> for kittycad_modeling_cmds::coord::Direction {
}
}
impl<'a> FromKclValue<'a> for crate::execution::Geometry {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Sketch { value } => Some(Self::Sketch(*value.to_owned())),
KclValue::Solid { value } => Some(Self::Solid(*value.to_owned())),
_ => None,
}
}
}
impl<'a> FromKclValue<'a> for crate::execution::GeometryWithImportedGeometry {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Sketch { value } => Some(Self::Sketch(*value.to_owned())),
KclValue::Solid { value } => Some(Self::Solid(*value.to_owned())),
KclValue::ImportedGeometry(value) => Some(Self::ImportedGeometry(Box::new(value.clone()))),
_ => None,
}
}
}
impl<'a> FromKclValue<'a> for FaceTag {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = || match arg.as_str() {
@ -1087,7 +1092,6 @@ impl<'a> FromKclValue<'a> for super::sketch::PlaneData {
origin: value.origin,
x_axis: value.x_axis,
y_axis: value.y_axis,
z_axis: value.z_axis,
});
}
// Case 1: predefined plane
@ -1108,13 +1112,7 @@ impl<'a> FromKclValue<'a> for super::sketch::PlaneData {
let origin = plane.get("origin").and_then(FromKclValue::from_kcl_val)?;
let x_axis = plane.get("xAxis").and_then(FromKclValue::from_kcl_val)?;
let y_axis = plane.get("yAxis").and_then(FromKclValue::from_kcl_val)?;
let z_axis = plane.get("zAxis").and_then(FromKclValue::from_kcl_val)?;
Some(Self::Plane {
origin,
x_axis,
y_axis,
z_axis,
})
Some(Self::Plane { origin, x_axis, y_axis })
}
}

View File

@ -0,0 +1,921 @@
//! Standard library clone.
use std::collections::HashMap;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd,
ok_response::{output::EntityGetAllChildUuids, OkModelingCmdResponse},
websocket::OkWebSocketResponseData,
ModelingCmd,
};
use kittycad_modeling_cmds::{self as kcmc};
use super::extrude::do_post_extrude;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
types::{NumericType, PrimitiveType, RuntimeType},
ExecState, GeometryWithImportedGeometry, KclValue, Sketch, Solid,
},
parsing::ast::types::TagNode,
std::{extrude::NamedCapTags, Args},
};
/// Clone a sketch or solid.
///
/// This works essentially like a copy-paste operation.
pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let geometry = args.get_unlabeled_kw_arg_typed(
"geometry",
&RuntimeType::Union(vec![
RuntimeType::Primitive(PrimitiveType::Sketch),
RuntimeType::Primitive(PrimitiveType::Solid),
RuntimeType::imported(),
]),
exec_state,
)?;
let cloned = inner_clone(geometry, exec_state, args).await?;
Ok(cloned.into())
}
/// Clone a sketch or solid.
///
/// This works essentially like a copy-paste operation.
///
/// This doesn't really have much utility unless you need the equivalent of a double
/// instance pattern with zero transformations.
///
/// Really only use this function if YOU ARE SURE you need it. In most cases you
/// do not need clone and using a pattern with `instance = 2` is more appropriate.
///
/// ```no_run
/// // Clone a basic sketch and move it and extrude it.
/// exampleSketch = startSketchOn(XY)
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// clonedSketch = clone(exampleSketch)
/// |> scale(
/// x = 1.0,
/// y = 1.0,
/// z = 2.5,
/// )
/// |> translate(
/// x = 15.0,
/// y = 0,
/// z = 0,
/// )
/// |> extrude(length = 5)
/// ```
///
/// ```no_run
/// // Clone a basic solid and move it.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// myPart = extrude(exampleSketch, length = 5)
/// clonedPart = clone(myPart)
/// |> translate(
/// x = 25.0,
/// )
/// ```
///
/// ```no_run
/// // Translate and rotate a cloned sketch to create a loft.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfileAt([-10, 10], %)
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20)
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> rotate(axis = [0, 0, 1.0], angle = 45)
///
/// loft([sketch001, sketch002])
/// ```
///
/// ```no_run
/// // Translate a cloned solid. Fillet only the clone.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfileAt([-10, 10], %)
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20, tag = $filletTag)
/// |> close()
/// |> extrude(length = 5)
///
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> fillet(
/// radius = 2,
/// tags = [getNextAdjacentEdge(filletTag)],
/// )
/// ```
///
/// ```no_run
/// // You can reuse the tags from the original geometry with the cloned geometry.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10], tag = $sketchingFace)
/// |> line(end = [-10, 0])
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 10, y = 20, z = 0)
/// |> extrude(length = 5)
///
/// startSketchOn(sketch002, face = sketchingFace)
/// |> startProfileAt([1, 1], %)
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close(tag = $sketchingFace002)
/// |> extrude(length = 10)
/// ```
///
/// ```no_run
/// // You can also use the tags from the original geometry to fillet the cloned geometry.
///
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 2
///
/// mountingPlateSketch = startSketchOn(XY)
/// |> startProfileAt([-width/2, -length/2], %)
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
///
/// clonedMountingPlate = clone(mountingPlate)
/// |> fillet(
/// radius = filletRadius,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// |> translate(x = 0, y = 50, z = 0)
/// ```
///
/// ```no_run
/// // Create a spring by sweeping around a helix path from a cloned sketch.
///
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 4,
/// length = 10,
/// radius = 5,
/// axis = Z,
/// )
///
///
/// springSketch = startSketchOn(YZ)
/// |> circle( center = [0, 0], radius = 1)
///
/// // Create a spring by sweeping around the helix path.
/// sweepedSpring = clone(springSketch)
/// |> translate(x=100)
/// |> sweep(path = helixPath)
/// ```
///
/// ```
/// // A donut shape from a cloned sketch.
/// sketch001 = startSketchOn(XY)
/// |> circle( center = [15, 0], radius = 5 )
///
/// sketch002 = clone(sketch001)
/// |> translate( z = 30)
/// |> revolve(
/// angle = 360,
/// axis = Y,
/// )
/// ```
///
/// ```no_run
/// // Sketch on the end of a revolved face by tagging the end face.
/// // This shows the cloned geometry will have the same tags as the original geometry.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfileAt([4, 12], %)
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
///
/// example001 = revolve(exampleSketch, axis = Y, angle = 180, tagEnd = $end01)
///
/// // example002 = clone(example001)
/// // |> translate(x = 0, y = 20, z = 0)
///
/// // Sketch on the cloned face.
/// // exampleSketch002 = startSketchOn(example002, face = end01)
/// // |> startProfileAt([4.5, -5], %)
/// // |> line(end = [0, 5])
/// // |> line(end = [5, 0])
/// // |> line(end = [0, -5])
/// // |> close()
///
/// // example003 = extrude(exampleSketch002, length = 5)
/// ```
///
/// ```no_run
/// // Clone an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// myCube = cube
///
/// clonedCube = clone(myCube)
/// |> translate(
/// x = 1020,
/// )
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "clone",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
geometry = { docs = "The sketch, solid, or imported geometry to be cloned" },
}
}]
async fn inner_clone(
geometry: GeometryWithImportedGeometry,
exec_state: &mut ExecState,
args: Args,
) -> Result<GeometryWithImportedGeometry, KclError> {
let new_id = exec_state.next_uuid();
let mut geometry = geometry.clone();
let old_id = geometry.id(&args.ctx).await?;
let mut new_geometry = match &geometry {
GeometryWithImportedGeometry::ImportedGeometry(imported) => {
let mut new_imported = imported.clone();
new_imported.id = new_id;
GeometryWithImportedGeometry::ImportedGeometry(new_imported)
}
GeometryWithImportedGeometry::Sketch(sketch) => {
let mut new_sketch = sketch.clone();
new_sketch.id = new_id;
new_sketch.original_id = new_id;
new_sketch.artifact_id = new_id.into();
GeometryWithImportedGeometry::Sketch(new_sketch)
}
GeometryWithImportedGeometry::Solid(solid) => {
let mut new_solid = solid.clone();
new_solid.id = new_id;
new_solid.sketch.original_id = new_id;
new_solid.artifact_id = new_id.into();
GeometryWithImportedGeometry::Solid(new_solid)
}
};
if args.ctx.no_engine_commands().await {
return Ok(new_geometry);
}
args.batch_modeling_cmd(new_id, ModelingCmd::from(mcmd::EntityClone { entity_id: old_id }))
.await?;
fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
.await
.map_err(|e| {
KclError::Internal(KclErrorDetails {
message: format!("failed to fix tags and references: {:?}", e),
source_ranges: vec![args.source_range],
})
})?;
Ok(new_geometry)
}
/// Fix the tags and references of the cloned geometry.
async fn fix_tags_and_references(
new_geometry: &mut GeometryWithImportedGeometry,
old_geometry_id: uuid::Uuid,
exec_state: &mut ExecState,
args: &Args,
) -> Result<()> {
let new_geometry_id = new_geometry.id(&args.ctx).await?;
let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
// Fix the path references in the new geometry.
match new_geometry {
GeometryWithImportedGeometry::ImportedGeometry(_) => {}
GeometryWithImportedGeometry::Sketch(sketch) => {
fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state).await?;
}
GeometryWithImportedGeometry::Solid(solid) => {
// Make the sketch id the new geometry id.
solid.sketch.id = new_geometry_id;
solid.sketch.original_id = new_geometry_id;
solid.sketch.artifact_id = new_geometry_id.into();
fix_sketch_tags_and_references(&mut solid.sketch, &entity_id_map, exec_state).await?;
let (start_tag, end_tag) = get_named_cap_tags(solid);
// Fix the edge cuts.
for edge_cut in solid.edge_cuts.iter_mut() {
let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) else {
anyhow::bail!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
};
edge_cut.set_edge_id(*new_edge_id);
let Some(id) = entity_id_map.get(&edge_cut.id()) else {
anyhow::bail!(
"Failed to find new edge cut id for old edge cut id: {:?}",
edge_cut.id()
);
};
edge_cut.set_id(*id);
}
// Do the after extrude things to update those ids, based on the new sketch
// information.
let new_solid = do_post_extrude(
&solid.sketch,
new_geometry_id.into(),
crate::std::args::TyF64::new(
solid.height,
NumericType::Known(crate::execution::types::UnitType::Length(solid.units)),
),
solid.sectional,
&NamedCapTags {
start: start_tag.as_ref(),
end: end_tag.as_ref(),
},
exec_state,
args,
)
.await?;
*solid = new_solid;
}
}
Ok(())
}
async fn get_old_new_child_map(
new_geometry_id: uuid::Uuid,
old_geometry_id: uuid::Uuid,
exec_state: &mut ExecState,
args: &Args,
) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
// Get the new geometries entity ids.
let response = args
.send_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityGetAllChildUuids {
entity_id: new_geometry_id,
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response:
OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
entity_ids: new_entity_ids,
}),
} = response
else {
anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
};
// Get the old geometries entity ids.
let response = args
.send_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EntityGetAllChildUuids {
entity_id: old_geometry_id,
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response:
OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
entity_ids: old_entity_ids,
}),
} = response
else {
anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
};
// Create a map of old entity ids to new entity ids.
Ok(HashMap::from_iter(
old_entity_ids
.iter()
.zip(new_entity_ids.iter())
.map(|(old_id, new_id)| (*old_id, *new_id)),
))
}
/// Fix the tags and references of a sketch.
async fn fix_sketch_tags_and_references(
new_sketch: &mut Sketch,
entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
exec_state: &mut ExecState,
) -> Result<()> {
// Fix the path references in the sketch.
for path in new_sketch.paths.as_mut_slice() {
let Some(new_path_id) = entity_id_map.get(&path.get_id()) else {
anyhow::bail!("Failed to find new path id for old path id: {:?}", path.get_id());
};
path.set_id(*new_path_id);
}
// Fix the tags
// This is annoying, in order to fix the tags we need to iterate over the paths again, but not
// mutable borrow the paths.
for path in new_sketch.paths.clone() {
// Check if this path has a tag.
if let Some(tag) = path.get_tag() {
new_sketch.add_tag(&tag, &path, exec_state);
}
}
// Fix the base path.
// TODO: Right now this one does not work, ignore for now and see if we really need it.
/* let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) else {
anyhow::bail!(
"Failed to find new base path id for old base path id: {:?}",
new_sketch.start.geo_meta.id
);
};
new_sketch.start.geo_meta.id = *new_base_path;*/
Ok(())
}
// Return the named cap tags for the original solid.
fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
let mut start_tag = None;
let mut end_tag = None;
// Check the start cap.
if let Some(start_cap_id) = solid.start_cap_id {
// Check if we had a value for that cap.
for value in &solid.value {
if value.get_id() == start_cap_id {
start_tag = value.get_tag().clone();
break;
}
}
}
// Check the end cap.
if let Some(end_cap_id) = solid.end_cap_id {
// Check if we had a value for that cap.
for value in &solid.value {
if value.get_id() == end_cap_id {
end_tag = value.get_tag().clone();
break;
}
}
}
(start_tag, end_tag)
}
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_ne};
use crate::exec::KclValue;
// Ensure the clone function returns a sketch with different ids for all the internal paths and
// the resulting sketch.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_sketch() {
let code = r#"cube = startSketchOn(XY)
|> startProfileAt([0,0], %)
|> line(end = [0, 10])
|> line(end = [10, 0])
|> line(end = [0, -10])
|> close()
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Sketch { value: cube } = cube else {
panic!("Expected a sketch, got: {:?}", cube);
};
let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
panic!("Expected a sketch, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.original_id, cloned_cube.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
assert_eq!(cloned_cube.original_id, cloned_cube.id);
for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
assert_eq!(cube.tags.len(), 0);
assert_eq!(cloned_cube.tags.len(), 0);
ctx.close().await;
}
// Ensure the clone function returns a solid with different ids for all the internal paths and
// references.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_solid() {
let code = r#"cube = startSketchOn(XY)
|> startProfileAt([0,0], %)
|> line(end = [0, 10])
|> line(end = [10, 0])
|> line(end = [0, -10])
|> close()
|> extrude(length = 5)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {:?}", cube);
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
assert_eq!(cube.sketch.tags.len(), 0);
assert_eq!(cloned_cube.sketch.tags.len(), 0);
assert_eq!(cube.edge_cuts.len(), 0);
assert_eq!(cloned_cube.edge_cuts.len(), 0);
ctx.close().await;
}
// Ensure the clone function returns a sketch with different ids for all the internal paths and
// the resulting sketch.
// AND TAGS.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_sketch_with_tags() {
let code = r#"cube = startSketchOn(XY)
|> startProfileAt([0,0], %) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Sketch { value: cube } = cube else {
panic!("Expected a sketch, got: {:?}", cube);
};
let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
panic!("Expected a sketch, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.original_id, cloned_cube.original_id);
for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (tag_name, tag) in &cube.tags {
let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_eq!(tag_info.surface, None);
assert_eq!(cloned_tag_info.surface, None);
}
ctx.close().await;
}
// Ensure the clone function returns a solid with different ids for all the internal paths and
// references.
// WITH TAGS.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_solid_with_tags() {
let code = r#"cube = startSketchOn(XY)
|> startProfileAt([0,0], %) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
|> extrude(length = 5) // TODO: Tag these
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {:?}", cube);
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (tag_name, tag) in &cube.sketch.tags {
let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_ne!(tag_info.surface, cloned_tag_info.surface);
}
assert_eq!(cube.edge_cuts.len(), 0);
assert_eq!(cloned_cube.edge_cuts.len(), 0);
ctx.close().await;
}
// Ensure we can get all paths even on a sketch where we closed it and it was already closed.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
async fn kcl_test_clone_cube_already_closed_sketch() {
let code = r#"// Clone a basic solid and move it.
exampleSketch = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line(end = [10, 0])
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> line(end = [0, -10])
|> close()
cube = extrude(exampleSketch, length = 5)
clonedCube = clone(cube)
|> translate(
x = 25.0,
)"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {:?}", cube);
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (tag_name, tag) in &cube.sketch.tags {
let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_ne!(tag_info.surface, cloned_tag_info.surface);
}
for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
assert_ne!(edge_cut.id(), cloned_edge_cut.id());
assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
}
ctx.close().await;
}
// Ensure the clone function returns a solid with different ids for all the internal paths and
// references.
// WITH TAGS AND EDGE CUTS.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "this test is not working yet, need to fix the edge cut ids"]
async fn kcl_test_clone_solid_with_edge_cuts() {
let code = r#"cube = startSketchOn(XY)
|> startProfileAt([0,0], %) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
|> extrude(length = 5) // TODO: Tag these
|> fillet(
radius = 2,
tags = [
getNextAdjacentEdge(tag02),
],
tag = $fillet01,
)
|> fillet(
radius = 2,
tags = [
getNextAdjacentEdge(tag04),
],
tag = $fillet02,
)
|> chamfer(
length = 2,
tags = [
getNextAdjacentEdge(tag03),
],
tag = $chamfer01,
)
|> chamfer(
length = 2,
tags = [
getNextAdjacentEdge(tag05),
],
tag = $chamfer02,
)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {:?}", cube);
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {:?}", cloned_cube);
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (tag_name, tag) in &cube.sketch.tags {
let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_ne!(tag_info.surface, cloned_tag_info.surface);
}
for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
assert_ne!(edge_cut.id(), cloned_edge_cut.id());
assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
}
ctx.close().await;
}
}

View File

@ -8,15 +8,15 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, ExtrudeSurface, KclValue, TagIdentifier},
execution::{types::RuntimeType, ExecState, ExtrudeSurface, KclValue, TagIdentifier},
std::Args,
};
/// Get the opposite edge to the edge given.
pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::edge(), exec_state)?;
let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
@ -53,15 +53,24 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
/// ```
#[stdlib {
name = "getOppositeEdge",
keywords = true,
unlabeled_first = true,
args = {
edge = { docs = "The tag of the edge you want to find the opposite edge of." },
}
}]
async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
async fn inner_get_opposite_edge(
edge: TagIdentifier,
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let resp = args
.send_modeling_cmd(
@ -88,9 +97,9 @@ async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState,
/// Get the next adjacent edge to the edge given.
pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::edge(), exec_state)?;
let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
@ -127,19 +136,24 @@ pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> R
/// ```
#[stdlib {
name = "getNextAdjacentEdge",
keywords = true,
unlabeled_first = true,
args = {
edge = { docs = "The tag of the edge you want to find the next adjacent edge of." },
}
}]
async fn inner_get_next_adjacent_edge(
tag: TagIdentifier,
edge: TagIdentifier,
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let resp = args
.send_modeling_cmd(
@ -167,7 +181,7 @@ async fn inner_get_next_adjacent_edge(
adjacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found next adjacent to tag: `{}`", tag.value),
message: format!("No edge found next adjacent to tag: `{}`", edge.value),
source_ranges: vec![args.source_range],
})
})
@ -175,9 +189,9 @@ async fn inner_get_next_adjacent_edge(
/// Get the previous adjacent edge to the edge given.
pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_data()?;
let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::edge(), exec_state)?;
let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
Ok(KclValue::Uuid {
value: edge,
meta: vec![args.source_range.into()],
@ -214,19 +228,24 @@ pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args)
/// ```
#[stdlib {
name = "getPreviousAdjacentEdge",
keywords = true,
unlabeled_first = true,
args = {
edge = { docs = "The tag of the edge you want to find the previous adjacent edge of." },
}
}]
async fn inner_get_previous_adjacent_edge(
tag: TagIdentifier,
edge: TagIdentifier,
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.no_engine_commands().await {
return Ok(exec_state.next_uuid());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
let id = exec_state.next_uuid();
let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
let resp = args
.send_modeling_cmd(
@ -253,7 +272,7 @@ async fn inner_get_previous_adjacent_edge(
adjacent_edge.edge.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("No edge found previous adjacent to tag: `{}`", tag.value),
message: format!("No edge found previous adjacent to tag: `{}`", edge.value),
source_ranges: vec![args.source_range],
})
})

View File

@ -399,6 +399,17 @@ pub(crate) async fn do_post_extrude<'a>(
});
Some(extrude_surface)
}
Path::CircularInvolute { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeInvolute(crate::execution::ExtrudeInvolute {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
}
} else if no_engine_commands {
// Only pre-populate the extrude surface if we are in mock mode.
@ -471,6 +482,7 @@ pub(crate) async fn do_post_extrude<'a>(
meta: sketch.meta.clone(),
units: sketch.units,
height: length.to_length_units(sketch.units),
sectional,
sketch,
start_cap_id,
end_cap_id,

View File

@ -6,6 +6,7 @@ pub mod array;
pub mod assert;
pub mod axis_or_reference;
pub mod chamfer;
pub mod clone;
pub mod convert;
pub mod csg;
pub mod edge;
@ -29,6 +30,7 @@ pub mod utils;
use anyhow::Result;
pub use args::Args;
use args::TyF64;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use lazy_static::lazy_static;
@ -39,7 +41,10 @@ use serde::{Deserialize, Serialize};
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{types::PrimitiveType, ExecState, KclValue},
execution::{
types::{NumericType, PrimitiveType, RuntimeType, UnitAngle, UnitType},
ExecState, KclValue,
},
parsing::ast::types::Name,
};
@ -67,8 +72,6 @@ lazy_static! {
Box::new(crate::std::segment::SegLen),
Box::new(crate::std::segment::SegAng),
Box::new(crate::std::segment::TangentToEnd),
Box::new(crate::std::segment::AngleToMatchLengthX),
Box::new(crate::std::segment::AngleToMatchLengthY),
Box::new(crate::std::shapes::CircleThreePoint),
Box::new(crate::std::shapes::Polygon),
Box::new(crate::std::sketch::InvoluteCircular),
@ -87,6 +90,7 @@ lazy_static! {
Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole),
Box::new(crate::std::clone::Clone),
Box::new(crate::std::patterns::PatternLinear2D),
Box::new(crate::std::patterns::PatternLinear3D),
Box::new(crate::std::patterns::PatternCircular2D),
@ -107,7 +111,6 @@ lazy_static! {
Box::new(crate::std::shell::Hollow),
Box::new(crate::std::sweep::Sweep),
Box::new(crate::std::loft::Loft),
Box::new(crate::std::planes::OffsetPlane),
Box::new(crate::std::math::Acos),
Box::new(crate::std::math::Asin),
Box::new(crate::std::math::Atan),
@ -205,6 +208,10 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|e, a| Box::pin(crate::std::revolve::revolve(e, a)),
StdFnProps::default("std::revolve").include_in_feature_tree(),
),
("prelude", "offsetPlane") => (
|e, a| Box::pin(crate::std::planes::offset_plane(e, a)),
StdFnProps::default("std::offsetPlane").include_in_feature_tree(),
),
_ => unreachable!(),
}
}
@ -284,8 +291,10 @@ pub enum FunctionKind {
const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// Compute the length of the given leg.
pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
pub async fn leg_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_length(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
@ -293,10 +302,16 @@ pub async fn leg_length(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
/// Compute the length of the given leg.
///
/// ```no_run
/// legLen(5, 3)
/// legLen(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legLen",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["utilities"],
}]
fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
@ -304,19 +319,31 @@ fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
}
/// Compute the angle of the given leg for x.
pub async fn leg_angle_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
pub async fn leg_angle_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_angle_x(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
Ok(KclValue::from_number_with_type(
result,
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
vec![args.into()],
))
}
/// Compute the angle of the given leg for x.
///
/// ```no_run
/// legAngX(5, 3)
/// legAngX(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legAngX",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["utilities"],
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
@ -324,19 +351,31 @@ fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
}
/// Compute the angle of the given leg for y.
pub async fn leg_angle_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hypotenuse, leg, ty) = args.get_hypotenuse_leg()?;
pub async fn leg_angle_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_angle_y(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
Ok(KclValue::from_number_with_type(
result,
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
vec![args.into()],
))
}
/// Compute the angle of the given leg for y.
///
/// ```no_run
/// legAngY(5, 3)
/// legAngY(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legAngY",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["utilities"],
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {

View File

@ -1,6 +1,5 @@
//! Standard library plane helpers.
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
@ -19,98 +18,6 @@ pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclV
Ok(KclValue::Plane { value: Box::new(plane) })
}
/// Offset a plane by a distance along its normal.
///
/// For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ'
/// plane and 10 units away from it.
///
/// ```no_run
/// // Loft a square and a circle on the `XY` plane using offset.
/// squareSketch = startSketchOn('XY')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('XY', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `XZ` plane using offset.
/// squareSketch = startSketchOn('XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('XZ', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `YZ` plane using offset.
/// squareSketch = startSketchOn('YZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('YZ', offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```no_run
/// // Loft a square and a circle on the `-XZ` plane using offset.
/// squareSketch = startSketchOn('-XZ')
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane('-XZ', offset = -150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
/// ```no_run
/// // A circle on the XY plane
/// startSketchOn("XY")
/// |> startProfileAt([0, 0], %)
/// |> circle( radius = 10, center = [0, 0] )
///
/// // Triangle on the plane 4 units above
/// startSketchOn(offsetPlane("XY", offset = 4))
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> close()
/// ```
#[stdlib {
name = "offsetPlane",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
plane = { docs = "The plane (e.g. XY) which this new plane is created from." },
offset = { docs = "Distance from the standard plane this new plane will be created at." },
}
}]
async fn inner_offset_plane(
plane: PlaneData,
offset: TyF64,
@ -122,7 +29,8 @@ async fn inner_offset_plane(
// standard planes themselves.
plane.value = PlaneType::Custom;
plane.origin += plane.z_axis * offset.to_length_units(plane.origin.units);
let normal = plane.x_axis.cross(&plane.y_axis);
plane.origin += normal * offset.to_length_units(plane.origin.units);
make_offset_plane_in_engine(&plane, exec_state, args).await?;
Ok(plane)

View File

@ -4,6 +4,7 @@ use anyhow::Result;
use kcl_derive_docs::stdlib;
use kittycad_modeling_cmds::shared::Angle;
use super::utils::untype_point;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -13,8 +14,6 @@ use crate::{
std::{args::TyF64, utils::between, Args},
};
use super::utils::untype_point;
/// Returns the point at the end of the given segment.
pub async fn segment_end(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let tag: TagIdentifier = args.get_unlabeled_kw_arg("tag")?;
@ -580,130 +579,3 @@ async fn inner_tangent_to_end(tag: &TagIdentifier, exec_state: &mut ExecState, a
Ok(previous_end_tangent.to_degrees())
}
/// Returns the angle to match the given length for x.
pub async fn angle_to_match_length_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_x(&tag, to, sketch, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::degrees())))
}
/// Returns the angle to match the given length for x.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [2, 5], tag = $seg01)
/// |> angledLine(
/// angle = -angleToMatchLengthX(seg01, 7, %),
/// endAbsoluteX = 10,
/// )
/// |> close()
///
/// extrusion = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "angleToMatchLengthX",
}]
fn inner_angle_to_match_length_x(
tag: &TagIdentifier,
to: TyF64,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let length = path.length().n;
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to.to_length_units(sketch.units) - last_line.to[0]).abs();
let angle_r = (diff / length).acos();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r.to_degrees())
}
}
/// Returns the angle to match the given length for y.
pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (tag, to, sketch) = args.get_tag_to_number_sketch()?;
let result = inner_angle_to_match_length_y(&tag, to, sketch, exec_state, args.clone())?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::degrees())))
}
/// Returns the angle to match the given length for y.
///
/// ```no_run
/// sketch001 = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
/// |> line(end = [1, 2], tag = $seg01)
/// |> angledLine(
/// angle = angleToMatchLengthY(seg01, 15, %),
/// length = 5,
/// )
/// |> yLine(endAbsolute = 0)
/// |> close()
///
/// extrusion = extrude(sketch001, length = 5)
/// ```
#[stdlib {
name = "angleToMatchLengthY",
}]
fn inner_angle_to_match_length_y(
tag: &TagIdentifier,
to: TyF64,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a line segment with a path, found `{:?}`", line),
source_ranges: vec![args.source_range],
})
})?;
let length = path.length().n;
let last_line = sketch
.paths
.last()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a Sketch with at least one segment, found `{:?}`", sketch),
source_ranges: vec![args.source_range],
})
})?
.get_base();
let diff = (to.to_length_units(sketch.units) - last_line.to[1]).abs();
let angle_r = (diff / length).asin();
if diff > length {
Ok(0.0)
} else {
Ok(angle_r.to_degrees())
}
}

View File

@ -105,6 +105,18 @@ pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result
let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
let reverse = args.get_kw_arg_opt("reverse")?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
if end_radius.n < start_radius.n {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: format!(
"endRadius: {0} cannot be less than startRadius: {1}",
end_radius.n, start_radius.n
)
.to_owned(),
}));
}
let new_sketch = inner_involute_circular(
sketch,
start_radius.n,
@ -198,7 +210,7 @@ async fn inner_involute_circular(
end.x += from.x;
end.y += from.y;
let current_path = Path::ToPoint {
let current_path = Path::CircularInvolute {
base: BasePath {
from: from.ignore_units(),
to: [end.x, end.y],
@ -209,6 +221,10 @@ async fn inner_involute_circular(
metadata: args.source_range.into(),
},
},
start_radius,
end_radius,
angle: angle.to_degrees(),
reverse: reverse.unwrap_or_default(),
};
let mut new_sketch = sketch.clone();
@ -960,9 +976,6 @@ pub enum PlaneData {
/// What should the planes Y axis be?
#[serde(rename = "yAxis")]
y_axis: Point3d,
/// The z-axis (normal).
#[serde(rename = "zAxis")]
z_axis: Point3d,
},
}
@ -1229,7 +1242,6 @@ async fn start_sketch_on_face(
// TODO: get this from the extrude plane data.
x_axis: solid.sketch.on.x_axis(),
y_axis: solid.sketch.on.y_axis(),
z_axis: solid.sketch.on.z_axis(),
units: solid.units,
solid,
meta: vec![args.source_range.into()],
@ -1247,49 +1259,18 @@ async fn make_sketch_plane_from_orientation(
let clobber = false;
let size = LengthUnit(60.0);
let hide = Some(true);
match data {
PlaneData::XY | PlaneData::NegXY | PlaneData::XZ | PlaneData::NegXZ | PlaneData::YZ | PlaneData::NegYZ => {
// TODO: ignoring the default planes here since we already created them, breaks the
// front end for the feature tree which is stupid and we should fix it.
let x_axis = match data {
PlaneData::NegXY => Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
PlaneData::NegXZ => Point3d::new(-1.0, 0.0, 0.0, UnitLen::Mm),
PlaneData::NegYZ => Point3d::new(0.0, -1.0, 0.0, UnitLen::Mm),
_ => plane.x_axis,
};
args.batch_modeling_cmd(
plane.id,
ModelingCmd::from(mcmd::MakePlane {
clobber,
origin: plane.origin.into(),
size,
x_axis: x_axis.into(),
y_axis: plane.y_axis.into(),
hide,
}),
)
.await?;
}
PlaneData::Plane {
origin,
x_axis,
y_axis,
z_axis: _,
} => {
args.batch_modeling_cmd(
plane.id,
ModelingCmd::from(mcmd::MakePlane {
clobber,
origin: origin.into(),
size,
x_axis: x_axis.into(),
y_axis: y_axis.into(),
hide,
}),
)
.await?;
}
}
args.batch_modeling_cmd(
plane.id,
ModelingCmd::from(mcmd::MakePlane {
clobber,
origin: plane.origin.into(),
size,
x_axis: plane.x_axis.into(),
y_axis: plane.y_axis.into(),
hide,
}),
)
.await?;
Ok(Box::new(plane))
}
@ -1384,7 +1365,8 @@ pub(crate) async fn inner_start_profile_at(
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
let normal = plane.x_axis.cross(&plane.y_axis);
Some(normal.into())
} else {
None
},

View File

@ -2219,8 +2219,8 @@ myAng2 = 134
part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([1, 3.82], %, $seg01) // ln-should-get-tag
|> angledLine(angle = -angleToMatchLengthX(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -angleToMatchLengthY(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
|> angledLine(angle = -foo(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -bar(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let recasted = program.recast(&Default::default(), 0);
@ -2237,8 +2237,8 @@ myAng2 = 134
part001 = startSketchOn(XY)
|> startProfileAt([0, 0], %)
|> line([1, 3.82], %, $seg01) // ln-should-get-tag
|> angledLine(angle = -angleToMatchLengthX(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -angleToMatchLengthY(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
|> angledLine(angle = -foo(seg01, myVar, %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
|> angledLine(angle = -bar(seg01, myVar, %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();

View File

@ -8,7 +8,7 @@ use anyhow::Result;
use crate::{
errors::KclErrorDetails,
modules::{ModulePath, ModuleRepr},
parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode, NodeRef, Program},
parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode},
walk::{Node, Visitable},
ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
};
@ -20,7 +20,7 @@ type Dependency = (String, String);
type Graph = Vec<Dependency>;
type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, AstNode<Program>);
type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, ModuleRepr);
type Universe = HashMap<String, DependencyInfo>;
/// Process a number of programs, returning the graph of dependencies.
@ -32,9 +32,9 @@ type Universe = HashMap<String, DependencyInfo>;
pub fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
let mut graph = Graph::new();
for (name, (_, _, _, program)) in progs.iter() {
for (name, (_, _, _, repr)) in progs.iter() {
graph.extend(
import_dependencies(program, ctx)?
import_dependencies(repr, ctx)?
.into_iter()
.map(|(dependency, _, _)| (name.clone(), dependency))
.collect::<Vec<_>>(),
@ -118,28 +118,42 @@ fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclEr
type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
pub(crate) fn import_dependencies(
prog: NodeRef<Program>,
ctx: &ExecutorContext,
) -> Result<ImportDependencies, KclError> {
let ret = Arc::new(Mutex::new(vec![]));
pub(crate) fn import_dependencies(repr: &ModuleRepr, ctx: &ExecutorContext) -> Result<ImportDependencies, KclError> {
let ModuleRepr::Kcl(prog, _) = repr else {
// It has no dependencies, so return an empty list.
return Ok(vec![]);
};
let ret = Arc::new(Mutex::new(vec![]));
fn walk(ret: Arc<Mutex<ImportDependencies>>, node: Node<'_>, ctx: &ExecutorContext) -> Result<(), KclError> {
if let Node::ImportStatement(is) = node {
// We only care about Kcl imports for now.
if let ImportPath::Kcl { filename } = &is.path {
let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory);
// We need to lock the mutex to push the dependency.
// This is a bit of a hack, but it works for now.
ret.lock()
.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to lock mutex: {}", err),
source_ranges: Default::default(),
})
})?
.push((filename.to_string(), is.clone(), resolved_path));
// We only care about Kcl and Foreign imports for now.
let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory);
match &is.path {
ImportPath::Kcl { filename } => {
// We need to lock the mutex to push the dependency.
// This is a bit of a hack, but it works for now.
ret.lock()
.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to lock mutex: {}", err),
source_ranges: Default::default(),
})
})?
.push((filename.to_string(), is.clone(), resolved_path));
}
ImportPath::Foreign { path } => {
ret.lock()
.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to lock mutex: {}", err),
source_ranges: Default::default(),
})
})?
.push((path.to_string(), is.clone(), resolved_path));
}
ImportPath::Std { .. } => { // do nothing
}
}
}
@ -164,11 +178,11 @@ pub(crate) fn import_dependencies(
pub(crate) async fn import_universe(
ctx: &ExecutorContext,
prog: NodeRef<'_, Program>,
repr: &ModuleRepr,
out: &mut Universe,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
let modules = import_dependencies(prog, ctx)?;
let modules = import_dependencies(repr, ctx)?;
for (filename, import_stmt, module_path) in modules {
if out.contains_key(&filename) {
continue;
@ -178,26 +192,21 @@ pub(crate) async fn import_universe(
.open_module(&import_stmt.path, &[], exec_state, Default::default())
.await?;
let program = {
let repr = {
let Some(module_info) = exec_state.get_module(module_id) else {
return Err(KclError::Internal(KclErrorDetails {
message: format!("Module {} not found", module_id),
source_ranges: vec![import_stmt.into()],
}));
};
let ModuleRepr::Kcl(program, _) = &module_info.repr else {
// if it's not a KCL module we can skip it since it has no
// dependencies.
continue;
};
program.clone()
module_info.repr.clone()
};
out.insert(
filename.clone(),
(import_stmt.clone(), module_id, module_path.clone(), program.clone()),
(import_stmt.clone(), module_id, module_path.clone(), repr.clone()),
);
Box::pin(import_universe(ctx, &program, out, exec_state)).await?;
Box::pin(import_universe(ctx, &repr, out, exec_state)).await?;
}
Ok(())
@ -206,7 +215,7 @@ pub(crate) async fn import_universe(
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::types::ImportSelector;
use crate::parsing::ast::types::{ImportSelector, Program};
macro_rules! kcl {
( $kcl:expr ) => {{
@ -224,7 +233,7 @@ mod tests {
}),
ModuleId::default(),
ModulePath::Local { value: "".into() },
program,
ModuleRepr::Kcl(program, None),
)
}

View File

@ -12,21 +12,18 @@ export XY = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 },
}: Plane
export XZ = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = -1, z = 0 },
}: Plane
export YZ = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 },
}: Plane
export X = {
@ -449,3 +446,93 @@ export fn toRadians(@num: number(rad)): number(rad) {
export fn toDegrees(@num: number(deg)): number(deg) {
return num
}
/// Offset a plane by a distance along its normal.
///
/// For example, if you offset the `XZ` plane by 10, the new plane will be parallel to the `XZ`
/// plane and 10 units away from it.
///
/// ```
/// // Loft a square and a circle on the `XY` plane using offset.
/// squareSketch = startSketchOn(XY)
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane(XY, offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```
/// // Loft a square and a circle on the `XZ` plane using offset.
/// squareSketch = startSketchOn(XZ)
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane(XZ, offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```
/// // Loft a square and a circle on the `YZ` plane using offset.
/// squareSketch = startSketchOn(YZ)
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane(YZ, offset = 150))
/// |> circle( center = [0, 100], radius = 50 )
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```
/// // Loft a square and a circle on the `-XZ` plane using offset.
/// squareSketch = startSketchOn(-XZ)
/// |> startProfileAt([-100, 200], %)
/// |> line(end = [200, 0])
/// |> line(end = [0, -200])
/// |> line(end = [-200, 0])
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = startSketchOn(offsetPlane(-XZ, offset = 150))
/// |> circle(center = [0, 100], radius = 50)
///
/// loft([squareSketch, circleSketch])
/// ```
///
/// ```
/// // A circle on the XY plane
/// startSketchOn(XY)
/// |> startProfileAt([0, 0], %)
/// |> circle( radius = 10, center = [0, 0] )
///
/// // Triangle on the plane 4 units above
/// startSketchOn(offsetPlane(XY, offset = 4))
/// |> startProfileAt([0, 0], %)
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> close()
/// ```
@(impl = std_rust)
export fn offsetPlane(
/// The plane (e.g. `XY`) which this new plane is created from.
@plane: Plane,
/// Distance from the standard plane this new plane will be created at.
offset: number(Length),
): Plane {}

View File

@ -212,14 +212,6 @@ description: Variables in memory after executing angled_line.kcl
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -257,7 +249,8 @@ description: Variables in memory after executing angled_line.kcl
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"seg01": {

View File

@ -191,14 +191,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -260,7 +252,8 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
],
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"extrude002": {
@ -394,14 +387,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -588,14 +573,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -657,7 +634,8 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
],
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -692,7 +670,8 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"seg01": {
@ -847,14 +826,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -996,14 +967,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -1190,14 +1153,6 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -1259,7 +1214,8 @@ description: Variables in memory after executing artifact_graph_example_code1.kc
],
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"

View File

@ -210,7 +210,7 @@ description: Artifact commands artifact_graph_example_code_no_3d.kcl
"planar_normal": {
"x": 0.0,
"y": 1.0,
"z": 0.0
"z": -0.0
}
}
},

View File

@ -166,14 +166,6 @@ description: Variables in memory after executing artifact_graph_example_code_no_
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -317,14 +309,6 @@ description: Variables in memory after executing artifact_graph_example_code_no_
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {

View File

@ -4,6 +4,15 @@ description: Operations executed artifact_graph_example_code_offset_planes.kcl
---
[
{
"type": "KclStdLibCall",
"name": "offsetPlane",
"unlabeledArg": {
"value": {
"type": "String",
"value": "XY"
},
"sourceRange": []
},
"labeledArgs": {
"offset": {
"value": {
@ -22,18 +31,18 @@ description: Operations executed artifact_graph_example_code_offset_planes.kcl
"sourceRange": []
}
},
"sourceRange": []
},
{
"type": "KclStdLibCall",
"name": "offsetPlane",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "String",
"value": "XY"
"value": "XZ"
},
"sourceRange": []
}
},
{
},
"labeledArgs": {
"offset": {
"value": {
@ -52,18 +61,18 @@ description: Operations executed artifact_graph_example_code_offset_planes.kcl
"sourceRange": []
}
},
"sourceRange": []
},
{
"type": "KclStdLibCall",
"name": "offsetPlane",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "String",
"value": "XZ"
"value": "YZ"
},
"sourceRange": []
}
},
{
},
"labeledArgs": {
"offset": {
"value": {
@ -82,16 +91,7 @@ description: Operations executed artifact_graph_example_code_offset_planes.kcl
"sourceRange": []
}
},
"name": "offsetPlane",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "String",
"value": "YZ"
},
"sourceRange": []
}
"sourceRange": []
},
{
"labeledArgs": {

View File

@ -32,14 +32,6 @@ description: Variables in memory after executing artifact_graph_example_code_off
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
}
},
@ -72,14 +64,6 @@ description: Variables in memory after executing artifact_graph_example_code_off
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
}
},
@ -112,14 +96,6 @@ description: Variables in memory after executing artifact_graph_example_code_off
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
}
},
@ -177,14 +153,6 @@ description: Variables in memory after executing artifact_graph_example_code_off
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {

View File

@ -153,14 +153,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -198,7 +190,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"extrude002": {
@ -332,14 +325,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -488,14 +473,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -533,7 +510,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -568,7 +546,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"extrude003": {
@ -714,14 +693,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -851,14 +822,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -1007,14 +970,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -1052,7 +1007,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -1087,7 +1043,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -1128,7 +1085,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"extrude004": {
@ -1262,14 +1220,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -1411,14 +1361,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -1548,14 +1490,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -1704,14 +1638,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -1749,7 +1675,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -1784,7 +1711,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -1825,7 +1753,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -1860,7 +1789,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"seg01": {
@ -1990,14 +1920,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -2135,14 +2057,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -2291,14 +2205,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -2336,7 +2242,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -2477,14 +2384,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -2614,14 +2513,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -2770,14 +2661,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -2815,7 +2698,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -2850,7 +2734,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -2991,14 +2876,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -3140,14 +3017,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -3277,14 +3146,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
},
"solid": {
"type": "Solid",
"id": "[uuid]",
@ -3433,14 +3294,6 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": -1.0,
"z": 0.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -3478,7 +3331,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -3513,7 +3367,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"
@ -3554,7 +3409,8 @@ description: Variables in memory after executing artifact_graph_sketch_on_face_e
"endCapId": "[uuid]",
"units": {
"type": "Mm"
}
},
"sectional": false
},
"units": {
"type": "Mm"

View File

@ -184,14 +184,6 @@ description: Variables in memory after executing basic_fillet_cube_close_opposit
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -275,7 +267,8 @@ description: Variables in memory after executing basic_fillet_cube_close_opposit
],
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"thing": {

View File

@ -172,14 +172,6 @@ description: Variables in memory after executing basic_fillet_cube_end.kcl
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -259,7 +251,8 @@ description: Variables in memory after executing basic_fillet_cube_end.kcl
],
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"thing": {

View File

@ -196,14 +196,6 @@ description: Variables in memory after executing basic_fillet_cube_next_adjacent
"units": {
"type": "Mm"
}
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Mm"
}
}
},
"start": {
@ -273,7 +265,8 @@ description: Variables in memory after executing basic_fillet_cube_next_adjacent
],
"units": {
"type": "Mm"
}
},
"sectional": false
}
},
"thing": {

Some files were not shown because too many files have changed in this diff Show More