Compare commits
31 Commits
jtran/upda
...
ben/involu
Author | SHA1 | Date | |
---|---|---|---|
3fcdf845c1 | |||
0ef1483e11 | |||
bbaaf86e4d | |||
6a0e10f8ab | |||
5c0ca52291 | |||
bff13f6bfe | |||
6001b71f06 | |||
bd1e68a4c8 | |||
8589f8fc5f | |||
6641e1178b | |||
6e0f1e71b2 | |||
0e945b6457 | |||
19155a9132 | |||
305d613d40 | |||
95f2caacab | |||
8f61ee1d2f | |||
b0594712f8 | |||
a3821f6229 | |||
b02dbd4fe6 | |||
668f6671a9 | |||
f6387eb7e9 | |||
83a87b046f | |||
20c2ce3bac | |||
2956f9ed55 | |||
510d74f2c7 | |||
457ab28f74 | |||
42082f72cd | |||
55baf58682 | |||
02bd211b70 | |||
37d34d45ff | |||
ed774e67b2 |
11
.github/workflows/e2e-tests.yml
vendored
@ -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
@ -0,0 +1 @@
|
||||
npm run fmt
|
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run fmt-check
|
258
docs/kcl/clone.md
Normal 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
```
|
||||
|
||||
|
||||
|
@ -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)
|
||||
```
|
||||
|
||||
|
||||
|
@ -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)
|
||||
```
|
||||
|
||||
|
||||
|
@ -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 if‑else.
|
||||
|
||||
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 re‑using geometry:
|
||||
|
||||
1. **Wrap the construction in a function** – flexible and fully parametric.
|
||||
2. **Duplicate an existing object with `clone`** – lightning‑fast, 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 per‑instance, you’re 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.
|
||||
|
||||
---
|
||||
|
||||
## Module‑level parallelism
|
||||
|
||||
Under the hood, the Design Studio runs **every module in parallel** where it can. This means:
|
||||
|
||||
- The top‑level 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.
|
||||
- CPU‑bound calculations in separate modules get their own worker threads.
|
||||
|
||||
### Why modules beat one‑big‑file
|
||||
|
||||
If you shoe‑horn 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 // non‑blocking
|
||||
|
||||
// ... 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 byte‑code. 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 – you’ve 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 deep‑dive for foreign‑file imports
|
||||
|
||||
Parallelized foreign‑file 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.
|
||||
```
|
||||
- Reads the file from disk and makes its API available.
|
||||
- Starts engine rendering but **does not block** your script.
|
||||
- This kick‑starts 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 kick‑starts the render pipeline but doesn’t block—you can continue other work while the Engine processes the model.
|
||||
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.
|
||||
|
||||
3. **Invocation (Blocking) Stage**
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube
|
||||
### Best practices
|
||||
|
||||
myCube = cube
|
||||
#### 1. Defer blocking calls
|
||||
|
||||
myCube
|
||||
|> translate(z=10) // <- This line blocks
|
||||
```
|
||||
- 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
|
||||
|
||||
##### 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 IO‑heavy work into its own module so it can render in parallel while `main.kcl` continues.
|
||||
|
||||
#### Future improvements
|
||||
|
||||
Upcoming releases will auto‑analyse dependencies and only block when truly necessary. Until then, explicit deferral will give you the best performance.
|
||||
|
||||
Upcoming releases will auto‑analyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.
|
||||
|
||||
|
122
docs/kcl/std-offsetPlane.md
Normal file
42407
docs/kcl/std.json
@ -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.
|
||||
|
||||
|
84
docs/kcl/types/GeometryWithImportedGeometry.md
Normal 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 |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
@ -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 |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
@ -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 |
|
||||
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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`,
|
||||
|
@ -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()
|
||||
|
@ -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:' })
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -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',
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
@ -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`,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
|
@ -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())
|
||||
|
@ -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/
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
@ -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,
|
||||
|
@ -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
|
||||
|
22
package.json
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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)
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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)
|
@ -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)
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 68 KiB |
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
921
rust/kcl-lib/src/std/clone.rs
Normal 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;
|
||||
}
|
||||
}
|
@ -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],
|
||||
})
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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 plane’s 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
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|