merge main
							
								
								
									
										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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -111819,10 +111819,10 @@
 | 
			
		||||
    "summary": "Get the next adjacent edge to the edge given.",
 | 
			
		||||
    "description": "",
 | 
			
		||||
    "tags": [],
 | 
			
		||||
    "keywordArguments": false,
 | 
			
		||||
    "keywordArguments": true,
 | 
			
		||||
    "args": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "tag",
 | 
			
		||||
        "name": "edge",
 | 
			
		||||
        "type": "TagIdentifier",
 | 
			
		||||
        "schema": {
 | 
			
		||||
          "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
 | 
			
		||||
@ -111839,7 +111839,8 @@
 | 
			
		||||
        },
 | 
			
		||||
        "required": true,
 | 
			
		||||
        "includeInSnippet": true,
 | 
			
		||||
        "labelRequired": true
 | 
			
		||||
        "description": "The tag of the edge you want to find the next adjacent edge of.",
 | 
			
		||||
        "labelRequired": false
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "returnValue": {
 | 
			
		||||
@ -111866,10 +111867,10 @@
 | 
			
		||||
    "summary": "Get the opposite edge to the edge given.",
 | 
			
		||||
    "description": "",
 | 
			
		||||
    "tags": [],
 | 
			
		||||
    "keywordArguments": false,
 | 
			
		||||
    "keywordArguments": true,
 | 
			
		||||
    "args": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "tag",
 | 
			
		||||
        "name": "edge",
 | 
			
		||||
        "type": "TagIdentifier",
 | 
			
		||||
        "schema": {
 | 
			
		||||
          "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
 | 
			
		||||
@ -111886,7 +111887,8 @@
 | 
			
		||||
        },
 | 
			
		||||
        "required": true,
 | 
			
		||||
        "includeInSnippet": true,
 | 
			
		||||
        "labelRequired": true
 | 
			
		||||
        "description": "The tag of the edge you want to find the opposite edge of.",
 | 
			
		||||
        "labelRequired": false
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "returnValue": {
 | 
			
		||||
@ -111913,10 +111915,10 @@
 | 
			
		||||
    "summary": "Get the previous adjacent edge to the edge given.",
 | 
			
		||||
    "description": "",
 | 
			
		||||
    "tags": [],
 | 
			
		||||
    "keywordArguments": false,
 | 
			
		||||
    "keywordArguments": true,
 | 
			
		||||
    "args": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "tag",
 | 
			
		||||
        "name": "edge",
 | 
			
		||||
        "type": "TagIdentifier",
 | 
			
		||||
        "schema": {
 | 
			
		||||
          "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
 | 
			
		||||
@ -111933,7 +111935,8 @@
 | 
			
		||||
        },
 | 
			
		||||
        "required": true,
 | 
			
		||||
        "includeInSnippet": true,
 | 
			
		||||
        "labelRequired": true
 | 
			
		||||
        "description": "The tag of the edge you want to find the previous adjacent edge of.",
 | 
			
		||||
        "labelRequired": false
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "returnValue": {
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB  | 
| 
		 Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB  | 
| 
		 Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB  | 
@ -1076,7 +1076,7 @@ mod test {
 | 
			
		||||
 | 
			
		||||
    #[for_each_std_mod]
 | 
			
		||||
    #[tokio::test(flavor = "multi_thread")]
 | 
			
		||||
    async fn test_examples() {
 | 
			
		||||
    async fn kcl_test_examples() {
 | 
			
		||||
        let std = walk_prelude();
 | 
			
		||||
        let mut errs = Vec::new();
 | 
			
		||||
        for d in std {
 | 
			
		||||
 | 
			
		||||
@ -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!(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
@ -2115,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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2579,6 +2579,28 @@ 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";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -659,13 +659,6 @@ 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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -559,6 +559,8 @@ clonedCube = clone(cube)
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
@ -615,6 +617,8 @@ clonedCube = clone(cube)
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
@ -668,6 +672,8 @@ clonedCube = clone(cube)
 | 
			
		||||
            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
 | 
			
		||||
@ -734,6 +740,8 @@ clonedCube = clone(cube)
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
@ -807,6 +815,8 @@ clonedCube = clone(cube)
 | 
			
		||||
            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
 | 
			
		||||
@ -905,5 +915,7 @@ clonedCube = clone(cube)
 | 
			
		||||
            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],
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB  | 
| 
		 Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB  | 
@ -0,0 +1,3 @@
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
 | 
			
		||||
clone(cube)
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Artifact graph flowchart multiple-foreign-imports-all-render.kcl
 | 
			
		||||
extension: md
 | 
			
		||||
snapshot_kind: binary
 | 
			
		||||
---
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
```mermaid
 | 
			
		||||
flowchart LR
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										504
									
								
								rust/kcl-lib/tests/multiple-foreign-imports-all-render/ast.snap
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,504 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Result of parsing multiple-foreign-imports-all-render.kcl
 | 
			
		||||
---
 | 
			
		||||
{
 | 
			
		||||
  "Ok": {
 | 
			
		||||
    "body": [
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "path": {
 | 
			
		||||
          "type": "Foreign",
 | 
			
		||||
          "path": "../inputs/cube.step"
 | 
			
		||||
        },
 | 
			
		||||
        "selector": {
 | 
			
		||||
          "type": "None",
 | 
			
		||||
          "alias": {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "name": "cube",
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "Identifier"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "ImportStatement",
 | 
			
		||||
        "type": "ImportStatement"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "path": {
 | 
			
		||||
          "type": "Kcl",
 | 
			
		||||
          "filename": "othercube.kcl"
 | 
			
		||||
        },
 | 
			
		||||
        "selector": {
 | 
			
		||||
          "type": "None",
 | 
			
		||||
          "alias": {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "name": "othercube",
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "Identifier"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "ImportStatement",
 | 
			
		||||
        "type": "ImportStatement"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "path": {
 | 
			
		||||
          "type": "Kcl",
 | 
			
		||||
          "filename": "anothercube.kcl"
 | 
			
		||||
        },
 | 
			
		||||
        "selector": {
 | 
			
		||||
          "type": "None",
 | 
			
		||||
          "alias": {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "name": "anothercube",
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "Identifier"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "ImportStatement",
 | 
			
		||||
        "type": "ImportStatement"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "declaration": {
 | 
			
		||||
          "commentStart": 0,
 | 
			
		||||
          "end": 0,
 | 
			
		||||
          "id": {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "name": "model",
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "Identifier"
 | 
			
		||||
          },
 | 
			
		||||
          "init": {
 | 
			
		||||
            "abs_path": false,
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "name": {
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "name": "cube",
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "Identifier"
 | 
			
		||||
            },
 | 
			
		||||
            "path": [],
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "Name",
 | 
			
		||||
            "type": "Name"
 | 
			
		||||
          },
 | 
			
		||||
          "start": 0,
 | 
			
		||||
          "type": "VariableDeclarator"
 | 
			
		||||
        },
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "kind": "const",
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "VariableDeclaration",
 | 
			
		||||
        "type": "VariableDeclaration"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "expression": {
 | 
			
		||||
          "body": [
 | 
			
		||||
            {
 | 
			
		||||
              "abs_path": false,
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "name": {
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": "othercube",
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Identifier"
 | 
			
		||||
              },
 | 
			
		||||
              "path": [],
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "Name",
 | 
			
		||||
              "type": "Name"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              "arguments": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "x",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "1020",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": {
 | 
			
		||||
                      "value": 1020.0,
 | 
			
		||||
                      "suffix": "None"
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              ],
 | 
			
		||||
              "callee": {
 | 
			
		||||
                "abs_path": false,
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": {
 | 
			
		||||
                  "commentStart": 0,
 | 
			
		||||
                  "end": 0,
 | 
			
		||||
                  "name": "translate",
 | 
			
		||||
                  "start": 0,
 | 
			
		||||
                  "type": "Identifier"
 | 
			
		||||
                },
 | 
			
		||||
                "path": [],
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Name"
 | 
			
		||||
              },
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "unlabeled": null
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              "arguments": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "color",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "\"#ff001f\"",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": "#ff001f"
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "metalness",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "50",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": {
 | 
			
		||||
                      "value": 50.0,
 | 
			
		||||
                      "suffix": "None"
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "roughness",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "50",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": {
 | 
			
		||||
                      "value": 50.0,
 | 
			
		||||
                      "suffix": "None"
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              ],
 | 
			
		||||
              "callee": {
 | 
			
		||||
                "abs_path": false,
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": {
 | 
			
		||||
                  "commentStart": 0,
 | 
			
		||||
                  "end": 0,
 | 
			
		||||
                  "name": "appearance",
 | 
			
		||||
                  "start": 0,
 | 
			
		||||
                  "type": "Identifier"
 | 
			
		||||
                },
 | 
			
		||||
                "path": [],
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Name"
 | 
			
		||||
              },
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "unlabeled": null
 | 
			
		||||
            }
 | 
			
		||||
          ],
 | 
			
		||||
          "commentStart": 0,
 | 
			
		||||
          "end": 0,
 | 
			
		||||
          "start": 0,
 | 
			
		||||
          "type": "PipeExpression",
 | 
			
		||||
          "type": "PipeExpression"
 | 
			
		||||
        },
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "ExpressionStatement",
 | 
			
		||||
        "type": "ExpressionStatement"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "commentStart": 0,
 | 
			
		||||
        "end": 0,
 | 
			
		||||
        "expression": {
 | 
			
		||||
          "body": [
 | 
			
		||||
            {
 | 
			
		||||
              "abs_path": false,
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "name": {
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": "anothercube",
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Identifier"
 | 
			
		||||
              },
 | 
			
		||||
              "path": [],
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "Name",
 | 
			
		||||
              "type": "Name"
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              "arguments": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "x",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "argument": {
 | 
			
		||||
                      "commentStart": 0,
 | 
			
		||||
                      "end": 0,
 | 
			
		||||
                      "raw": "1020",
 | 
			
		||||
                      "start": 0,
 | 
			
		||||
                      "type": "Literal",
 | 
			
		||||
                      "type": "Literal",
 | 
			
		||||
                      "value": {
 | 
			
		||||
                        "value": 1020.0,
 | 
			
		||||
                        "suffix": "None"
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "operator": "-",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "UnaryExpression",
 | 
			
		||||
                    "type": "UnaryExpression"
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              ],
 | 
			
		||||
              "callee": {
 | 
			
		||||
                "abs_path": false,
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": {
 | 
			
		||||
                  "commentStart": 0,
 | 
			
		||||
                  "end": 0,
 | 
			
		||||
                  "name": "translate",
 | 
			
		||||
                  "start": 0,
 | 
			
		||||
                  "type": "Identifier"
 | 
			
		||||
                },
 | 
			
		||||
                "path": [],
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Name"
 | 
			
		||||
              },
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "unlabeled": null
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              "arguments": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "color",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "\"#ff0000\"",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": "#ff0000"
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "metalness",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "50",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": {
 | 
			
		||||
                      "value": 50.0,
 | 
			
		||||
                      "suffix": "None"
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "LabeledArg",
 | 
			
		||||
                  "label": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "name": "roughness",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Identifier"
 | 
			
		||||
                  },
 | 
			
		||||
                  "arg": {
 | 
			
		||||
                    "commentStart": 0,
 | 
			
		||||
                    "end": 0,
 | 
			
		||||
                    "raw": "50",
 | 
			
		||||
                    "start": 0,
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "type": "Literal",
 | 
			
		||||
                    "value": {
 | 
			
		||||
                      "value": 50.0,
 | 
			
		||||
                      "suffix": "None"
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              ],
 | 
			
		||||
              "callee": {
 | 
			
		||||
                "abs_path": false,
 | 
			
		||||
                "commentStart": 0,
 | 
			
		||||
                "end": 0,
 | 
			
		||||
                "name": {
 | 
			
		||||
                  "commentStart": 0,
 | 
			
		||||
                  "end": 0,
 | 
			
		||||
                  "name": "appearance",
 | 
			
		||||
                  "start": 0,
 | 
			
		||||
                  "type": "Identifier"
 | 
			
		||||
                },
 | 
			
		||||
                "path": [],
 | 
			
		||||
                "start": 0,
 | 
			
		||||
                "type": "Name"
 | 
			
		||||
              },
 | 
			
		||||
              "commentStart": 0,
 | 
			
		||||
              "end": 0,
 | 
			
		||||
              "start": 0,
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "type": "CallExpressionKw",
 | 
			
		||||
              "unlabeled": null
 | 
			
		||||
            }
 | 
			
		||||
          ],
 | 
			
		||||
          "commentStart": 0,
 | 
			
		||||
          "end": 0,
 | 
			
		||||
          "start": 0,
 | 
			
		||||
          "type": "PipeExpression",
 | 
			
		||||
          "type": "PipeExpression"
 | 
			
		||||
        },
 | 
			
		||||
        "start": 0,
 | 
			
		||||
        "type": "ExpressionStatement",
 | 
			
		||||
        "type": "ExpressionStatement"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "commentStart": 0,
 | 
			
		||||
    "end": 0,
 | 
			
		||||
    "nonCodeMeta": {
 | 
			
		||||
      "nonCodeNodes": {
 | 
			
		||||
        "2": [
 | 
			
		||||
          {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "NonCodeNode",
 | 
			
		||||
            "value": {
 | 
			
		||||
              "type": "newLine"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "3": [
 | 
			
		||||
          {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "NonCodeNode",
 | 
			
		||||
            "value": {
 | 
			
		||||
              "type": "newLine"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "4": [
 | 
			
		||||
          {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "NonCodeNode",
 | 
			
		||||
            "value": {
 | 
			
		||||
              "type": "newLine"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "5": [
 | 
			
		||||
          {
 | 
			
		||||
            "commentStart": 0,
 | 
			
		||||
            "end": 0,
 | 
			
		||||
            "start": 0,
 | 
			
		||||
            "type": "NonCodeNode",
 | 
			
		||||
            "value": {
 | 
			
		||||
              "type": "newLine"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "startNodes": []
 | 
			
		||||
    },
 | 
			
		||||
    "start": 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
import "othercube.kcl" as othercube
 | 
			
		||||
import "anothercube.kcl" as anothercube
 | 
			
		||||
 | 
			
		||||
model = cube
 | 
			
		||||
 | 
			
		||||
othercube
 | 
			
		||||
    |> translate(x=1020)
 | 
			
		||||
    |> appearance(
 | 
			
		||||
       color = "#ff001f",
 | 
			
		||||
       metalness = 50,
 | 
			
		||||
       roughness = 50
 | 
			
		||||
   )
 | 
			
		||||
 | 
			
		||||
anothercube
 | 
			
		||||
    |> translate(x=-1020)
 | 
			
		||||
    |> appearance(
 | 
			
		||||
       color = "#ff0000",
 | 
			
		||||
       metalness = 50,
 | 
			
		||||
       roughness = 50
 | 
			
		||||
   )
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,96 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Operations executed multiple-foreign-imports-all-render.kcl
 | 
			
		||||
---
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupBegin",
 | 
			
		||||
    "group": {
 | 
			
		||||
      "type": "ModuleInstance",
 | 
			
		||||
      "name": "cube",
 | 
			
		||||
      "moduleId": 6
 | 
			
		||||
    },
 | 
			
		||||
    "sourceRange": []
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupEnd"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupBegin",
 | 
			
		||||
    "group": {
 | 
			
		||||
      "type": "ModuleInstance",
 | 
			
		||||
      "name": "othercube",
 | 
			
		||||
      "moduleId": 7
 | 
			
		||||
    },
 | 
			
		||||
    "sourceRange": []
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupBegin",
 | 
			
		||||
    "group": {
 | 
			
		||||
      "type": "ModuleInstance",
 | 
			
		||||
      "name": "cube",
 | 
			
		||||
      "moduleId": 6
 | 
			
		||||
    },
 | 
			
		||||
    "sourceRange": []
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupEnd"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "labeledArgs": {
 | 
			
		||||
      "geometry": {
 | 
			
		||||
        "value": {
 | 
			
		||||
          "type": "ImportedGeometry",
 | 
			
		||||
          "artifact_id": "[uuid]"
 | 
			
		||||
        },
 | 
			
		||||
        "sourceRange": []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "name": "clone",
 | 
			
		||||
    "sourceRange": [],
 | 
			
		||||
    "type": "StdLibCall",
 | 
			
		||||
    "unlabeledArg": null
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupEnd"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupBegin",
 | 
			
		||||
    "group": {
 | 
			
		||||
      "type": "ModuleInstance",
 | 
			
		||||
      "name": "anothercube",
 | 
			
		||||
      "moduleId": 8
 | 
			
		||||
    },
 | 
			
		||||
    "sourceRange": []
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupBegin",
 | 
			
		||||
    "group": {
 | 
			
		||||
      "type": "ModuleInstance",
 | 
			
		||||
      "name": "cube",
 | 
			
		||||
      "moduleId": 6
 | 
			
		||||
    },
 | 
			
		||||
    "sourceRange": []
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupEnd"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "labeledArgs": {
 | 
			
		||||
      "geometry": {
 | 
			
		||||
        "value": {
 | 
			
		||||
          "type": "ImportedGeometry",
 | 
			
		||||
          "artifact_id": "[uuid]"
 | 
			
		||||
        },
 | 
			
		||||
        "sourceRange": []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "name": "clone",
 | 
			
		||||
    "sourceRange": [],
 | 
			
		||||
    "type": "StdLibCall",
 | 
			
		||||
    "unlabeledArg": null
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "type": "GroupEnd"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
 | 
			
		||||
clone(cube)
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Variables in memory after executing multiple-foreign-imports-all-render.kcl
 | 
			
		||||
---
 | 
			
		||||
{
 | 
			
		||||
  "anothercube": {
 | 
			
		||||
    "type": "Module",
 | 
			
		||||
    "value": 8
 | 
			
		||||
  },
 | 
			
		||||
  "cube": {
 | 
			
		||||
    "type": "Module",
 | 
			
		||||
    "value": 6
 | 
			
		||||
  },
 | 
			
		||||
  "model": {
 | 
			
		||||
    "type": "ImportedGeometry",
 | 
			
		||||
    "id": "[uuid]",
 | 
			
		||||
    "value": [
 | 
			
		||||
      "cube.step"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "othercube": {
 | 
			
		||||
    "type": "Module",
 | 
			
		||||
    "value": 7
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 81 KiB  | 
@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Result of unparsing multiple-foreign-imports-all-render.kcl
 | 
			
		||||
---
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
import "othercube.kcl" as othercube
 | 
			
		||||
import "anothercube.kcl" as anothercube
 | 
			
		||||
 | 
			
		||||
model = cube
 | 
			
		||||
 | 
			
		||||
othercube
 | 
			
		||||
  |> translate(x = 1020)
 | 
			
		||||
  |> appearance(color = "#ff001f", metalness = 50, roughness = 50)
 | 
			
		||||
 | 
			
		||||
anothercube
 | 
			
		||||
  |> translate(x = -1020)
 | 
			
		||||
  |> appearance(color = "#ff0000", metalness = 50, roughness = 50)
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Result of unparsing tests/multiple-foreign-imports-all-render/anothercube.kcl
 | 
			
		||||
---
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
 | 
			
		||||
clone(cube)
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
source: kcl-lib/src/simulation_tests.rs
 | 
			
		||||
description: Result of unparsing tests/multiple-foreign-imports-all-render/othercube.kcl
 | 
			
		||||
---
 | 
			
		||||
import "../inputs/cube.step" as cube
 | 
			
		||||
 | 
			
		||||
clone(cube)
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB  | 
| 
		 Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB  | 
| 
		 Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB  | 
| 
		 Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB  | 
| 
		 Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB  | 
							
								
								
									
										59
									
								
								scripts/diff.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,59 @@
 | 
			
		||||
const fs = require('fs')
 | 
			
		||||
const latestRun = fs.readFileSync('/tmp/circular-deps.txt','utf-8')
 | 
			
		||||
const knownCircular = fs.readFileSync('./known-circular.txt','utf-8')
 | 
			
		||||
 | 
			
		||||
function parseLine (line) {
 | 
			
		||||
  let num = null
 | 
			
		||||
  let depPath = null
 | 
			
		||||
  const res = line.split(")",2)
 | 
			
		||||
  if (res.length === 2) {
 | 
			
		||||
    // should be a dep line
 | 
			
		||||
    num = parseInt(res[0])
 | 
			
		||||
    depPath = res[1]
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    num,
 | 
			
		||||
    depPath
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeDependencyHash (file) {
 | 
			
		||||
  const deps = {}
 | 
			
		||||
  file.split("\n").forEach((line)=>{
 | 
			
		||||
    const {num, depPath} = parseLine(line)
 | 
			
		||||
    if (depPath && !isNaN(num)) {
 | 
			
		||||
      deps[depPath] = 1
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  return deps
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const latestRunDepHash = makeDependencyHash(latestRun)
 | 
			
		||||
const knownDepHash = makeDependencyHash(knownCircular)
 | 
			
		||||
 | 
			
		||||
const dup1 = JSON.parse(JSON.stringify(latestRunDepHash))
 | 
			
		||||
const dup2 = JSON.parse(JSON.stringify(knownDepHash))
 | 
			
		||||
Object.keys(knownDepHash).forEach((key)=>{
 | 
			
		||||
  delete dup1[key]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
Object.keys(latestRunDepHash).forEach((key)=>{
 | 
			
		||||
  delete dup2[key]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
console.log(" ")
 | 
			
		||||
console.log("diff.js - line item diff")
 | 
			
		||||
console.log(" ")
 | 
			
		||||
console.log("Added(+)")
 | 
			
		||||
Object.keys(dup1).forEach((dep, index)=>{
 | 
			
		||||
  console.log(`${index+1}) ${dep}`)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
console.log(" ")
 | 
			
		||||
console.log("Removed(-)")
 | 
			
		||||
if (Object.keys(dup2).length === 0) {
 | 
			
		||||
  console.log("None")
 | 
			
		||||
}
 | 
			
		||||
Object.keys(dup2).forEach((dep, index)=>{
 | 
			
		||||
  console.log(`${index+1}) ${dep}`)
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										10
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						@ -28,13 +28,9 @@ import { PATHS } from '@src/lib/paths'
 | 
			
		||||
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
 | 
			
		||||
import { sceneInfra } from '@src/lib/singletons'
 | 
			
		||||
import { maybeWriteToDisk } from '@src/lib/telemetry'
 | 
			
		||||
import type { IndexLoaderData } from '@src/lib/types'
 | 
			
		||||
import {
 | 
			
		||||
  engineStreamActor,
 | 
			
		||||
  useSettings,
 | 
			
		||||
  useToken,
 | 
			
		||||
} from '@src/machines/appMachine'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import { type IndexLoaderData } from '@src/lib/types'
 | 
			
		||||
import { engineStreamActor, useSettings, useToken } from '@src/lib/singletons'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
 | 
			
		||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
 | 
			
		||||
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import Loading from '@src/components/Loading'
 | 
			
		||||
import { useAuthState } from '@src/machines/appMachine'
 | 
			
		||||
import { useAuthState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
// Wrapper around protected routes, used in src/Router.tsx
 | 
			
		||||
export const Auth = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								src/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,35 @@
 | 
			
		||||
import { AppStateProvider } from '@src/AppState'
 | 
			
		||||
import LspProvider from '@src/components/LspProvider'
 | 
			
		||||
import { MachineManagerProvider } from '@src/components/MachineManagerProvider'
 | 
			
		||||
import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler'
 | 
			
		||||
import { SystemIOMachineLogicListenerDesktop } from '@src/components/Providers/SystemIOProviderDesktop'
 | 
			
		||||
import { SystemIOMachineLogicListenerWeb } from '@src/components/Providers/SystemIOProviderWeb'
 | 
			
		||||
import { RouteProvider } from '@src/components/RouteProvider'
 | 
			
		||||
import { KclContextProvider } from '@src/lang/KclProvider'
 | 
			
		||||
import { Outlet } from 'react-router-dom'
 | 
			
		||||
import { isDesktop } from '@src/lib/isDesktop'
 | 
			
		||||
// Root component will live for the entire applications runtime
 | 
			
		||||
function RootLayout() {
 | 
			
		||||
  return (
 | 
			
		||||
    <OpenInDesktopAppHandler>
 | 
			
		||||
      <RouteProvider>
 | 
			
		||||
        <LspProvider>
 | 
			
		||||
          <KclContextProvider>
 | 
			
		||||
            <AppStateProvider>
 | 
			
		||||
              <MachineManagerProvider>
 | 
			
		||||
                {isDesktop() ? (
 | 
			
		||||
                  <SystemIOMachineLogicListenerDesktop />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <SystemIOMachineLogicListenerWeb />
 | 
			
		||||
                )}
 | 
			
		||||
                <Outlet />
 | 
			
		||||
              </MachineManagerProvider>
 | 
			
		||||
            </AppStateProvider>
 | 
			
		||||
          </KclContextProvider>
 | 
			
		||||
        </LspProvider>
 | 
			
		||||
      </RouteProvider>
 | 
			
		||||
    </OpenInDesktopAppHandler>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default RootLayout
 | 
			
		||||
@ -9,22 +9,15 @@ import {
 | 
			
		||||
} from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { App } from '@src/App'
 | 
			
		||||
import { AppStateProvider } from '@src/AppState'
 | 
			
		||||
import { Auth } from '@src/Auth'
 | 
			
		||||
import { CommandBar } from '@src/components/CommandBar/CommandBar'
 | 
			
		||||
import DownloadAppBanner from '@src/components/DownloadAppBanner'
 | 
			
		||||
import { ErrorPage } from '@src/components/ErrorPage'
 | 
			
		||||
import FileMachineProvider from '@src/components/FileMachineProvider'
 | 
			
		||||
import LspProvider from '@src/components/LspProvider'
 | 
			
		||||
import { MachineManagerProvider } from '@src/components/MachineManagerProvider'
 | 
			
		||||
import ModelingMachineProvider from '@src/components/ModelingMachineProvider'
 | 
			
		||||
import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler'
 | 
			
		||||
import { ProjectsContextProvider } from '@src/components/ProjectsContextProvider'
 | 
			
		||||
import { RouteProvider } from '@src/components/RouteProvider'
 | 
			
		||||
import { WasmErrBanner } from '@src/components/WasmErrBanner'
 | 
			
		||||
import { NetworkContext } from '@src/hooks/useNetworkContext'
 | 
			
		||||
import { useNetworkStatus } from '@src/hooks/useNetworkStatus'
 | 
			
		||||
import { KclContextProvider } from '@src/lang/KclProvider'
 | 
			
		||||
import { coreDump } from '@src/lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  ASK_TO_OPEN_QUERY_PARAM,
 | 
			
		||||
@ -42,7 +35,8 @@ import {
 | 
			
		||||
  rustContext,
 | 
			
		||||
} from '@src/lib/singletons'
 | 
			
		||||
import { reportRejection } from '@src/lib/trap'
 | 
			
		||||
import { useToken } from '@src/machines/appMachine'
 | 
			
		||||
import { useToken } from '@src/lib/singletons'
 | 
			
		||||
import RootLayout from '@src/Root'
 | 
			
		||||
import Home from '@src/routes/Home'
 | 
			
		||||
import Onboarding, { onboardingRoutes } from '@src/routes/Onboarding'
 | 
			
		||||
import { Settings } from '@src/routes/Settings'
 | 
			
		||||
@ -54,27 +48,13 @@ const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
 | 
			
		||||
const router = createRouter([
 | 
			
		||||
  {
 | 
			
		||||
    id: PATHS.INDEX,
 | 
			
		||||
    element: (
 | 
			
		||||
      <OpenInDesktopAppHandler>
 | 
			
		||||
        <RouteProvider>
 | 
			
		||||
          <LspProvider>
 | 
			
		||||
            <ProjectsContextProvider>
 | 
			
		||||
              <KclContextProvider>
 | 
			
		||||
                <AppStateProvider>
 | 
			
		||||
                  <MachineManagerProvider>
 | 
			
		||||
                    <Outlet />
 | 
			
		||||
                  </MachineManagerProvider>
 | 
			
		||||
                </AppStateProvider>
 | 
			
		||||
              </KclContextProvider>
 | 
			
		||||
            </ProjectsContextProvider>
 | 
			
		||||
          </LspProvider>
 | 
			
		||||
        </RouteProvider>
 | 
			
		||||
      </OpenInDesktopAppHandler>
 | 
			
		||||
    ),
 | 
			
		||||
    errorElement: <ErrorPage />,
 | 
			
		||||
    element: <RootLayout />,
 | 
			
		||||
    // Gotcha: declaring errorElement on the root will unmount the element causing our forever React components to unmount.
 | 
			
		||||
    // Leave errorElement on the child components, this allows for the entire react context on error pages as well.
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: PATHS.INDEX,
 | 
			
		||||
        errorElement: <ErrorPage />,
 | 
			
		||||
        loader: async ({ request }) => {
 | 
			
		||||
          const onDesktop = isDesktop()
 | 
			
		||||
          const url = new URL(request.url)
 | 
			
		||||
@ -95,6 +75,7 @@ const router = createRouter([
 | 
			
		||||
        loader: fileLoader,
 | 
			
		||||
        id: PATHS.FILE,
 | 
			
		||||
        path: PATHS.FILE + '/:id',
 | 
			
		||||
        errorElement: <ErrorPage />,
 | 
			
		||||
        element: (
 | 
			
		||||
          <Auth>
 | 
			
		||||
            <FileMachineProvider>
 | 
			
		||||
@ -141,6 +122,7 @@ const router = createRouter([
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: PATHS.HOME,
 | 
			
		||||
        errorElement: <ErrorPage />,
 | 
			
		||||
        element: (
 | 
			
		||||
          <Auth>
 | 
			
		||||
            <Outlet />
 | 
			
		||||
@ -169,6 +151,7 @@ const router = createRouter([
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: PATHS.SIGN_IN,
 | 
			
		||||
        errorElement: <ErrorPage />,
 | 
			
		||||
        element: <SignIn />,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ import type {
 | 
			
		||||
} from '@src/lib/toolbar'
 | 
			
		||||
import { isToolbarItemResolvedDropdown, toolbarConfig } from '@src/lib/toolbar'
 | 
			
		||||
import { isArray } from '@src/lib/utils'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
export function Toolbar({
 | 
			
		||||
  className = '',
 | 
			
		||||
 | 
			
		||||
@ -40,8 +40,8 @@ import {
 | 
			
		||||
} from '@src/lib/singletons'
 | 
			
		||||
import { err, reportRejection, trap } from '@src/lib/trap'
 | 
			
		||||
import { throttle, toSync } from '@src/lib/utils'
 | 
			
		||||
import type { useSettings } from '@src/machines/appMachine'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import type { useSettings } from '@src/lib/singletons'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
import type { SegmentOverlay } from '@src/machines/modelingMachine'
 | 
			
		||||
 | 
			
		||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
 | 
			
		||||
 | 
			
		||||
@ -84,7 +84,7 @@ import { getThemeColorForThreeJs } from '@src/lib/theme'
 | 
			
		||||
import { err } from '@src/lib/trap'
 | 
			
		||||
import { isClockwise, normaliseAngle, roundOff } from '@src/lib/utils'
 | 
			
		||||
import { getTangentPointFromPreviousArc } from '@src/lib/utils2d'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
import type {
 | 
			
		||||
  SegmentOverlay,
 | 
			
		||||
  SegmentOverlayPayload,
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'
 | 
			
		||||
import UserSidebarMenu from '@src/components/UserSidebarMenu'
 | 
			
		||||
import { isDesktop } from '@src/lib/isDesktop'
 | 
			
		||||
import { type IndexLoaderData } from '@src/lib/types'
 | 
			
		||||
import { useUser } from '@src/machines/appMachine'
 | 
			
		||||
import { useUser } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
import styles from './AppHeader.module.css'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Switch } from '@headlessui/react'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
import { settingsActor, useSettings } from '@src/machines/appMachine'
 | 
			
		||||
import { settingsActor, useSettings } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
export function CameraProjectionToggle() {
 | 
			
		||||
  const settings = useSettings()
 | 
			
		||||
 | 
			
		||||
@ -8,10 +8,7 @@ import type {
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentOption,
 | 
			
		||||
} from '@src/lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
const contextSelector = (snapshot: StateFrom<AnyStateMachine> | undefined) =>
 | 
			
		||||
  snapshot?.context
 | 
			
		||||
 | 
			
		||||
@ -11,10 +11,7 @@ import { useNetworkContext } from '@src/hooks/useNetworkContext'
 | 
			
		||||
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
 | 
			
		||||
import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
 | 
			
		||||
import { engineCommandManager } from '@src/lib/singletons'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
 | 
			
		||||
 | 
			
		||||
@ -7,10 +7,7 @@ import CommandBarSelectionInput from '@src/components/CommandBar/CommandBarSelec
 | 
			
		||||
import CommandBarSelectionMixedInput from '@src/components/CommandBar/CommandBarSelectionMixedInput'
 | 
			
		||||
import CommandBarTextareaInput from '@src/components/CommandBar/CommandBarTextareaInput'
 | 
			
		||||
import type { CommandArgument } from '@src/lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const commandBarState = useCommandBarState()
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,7 @@ import { useEffect, useMemo, useRef } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
import type { CommandArgument } from '@src/lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
// TODO: remove the need for this selector once we decouple all actors from React
 | 
			
		||||
 | 
			
		||||
@ -11,10 +11,7 @@ import type {
 | 
			
		||||
import type { Selections } from '@src/lib/selections'
 | 
			
		||||
import { getSelectionTypeDisplayText } from '@src/lib/selections'
 | 
			
		||||
import { roundOff } from '@src/lib/utils'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
function CommandBarHeader({ children }: React.PropsWithChildren<object>) {
 | 
			
		||||
  const commandBarState = useCommandBarState()
 | 
			
		||||
 | 
			
		||||
@ -29,11 +29,8 @@ import { err } from '@src/lib/trap'
 | 
			
		||||
import { useCalculateKclExpression } from '@src/lib/useCalculateKclExpression'
 | 
			
		||||
import { roundOff } from '@src/lib/utils'
 | 
			
		||||
import { varMentions } from '@src/lib/varCompletionExtension'
 | 
			
		||||
import { useSettings } from '@src/machines/appMachine'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { useSettings } from '@src/lib/singletons'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
import styles from './CommandBarKclInput.module.css'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,10 +5,7 @@ import { ActionButton } from '@src/components/ActionButton'
 | 
			
		||||
import type { CommandArgument } from '@src/lib/commandTypes'
 | 
			
		||||
import { reportRejection } from '@src/lib/trap'
 | 
			
		||||
import { isArray, toSync } from '@src/lib/utils'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
import { useSelector } from '@xstate/react'
 | 
			
		||||
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,7 @@
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const commandBarState = useCommandBarState()
 | 
			
		||||
 | 
			
		||||
@ -12,10 +12,7 @@ import {
 | 
			
		||||
import { engineCommandManager, kclManager } from '@src/lib/singletons'
 | 
			
		||||
import { reportRejection } from '@src/lib/trap'
 | 
			
		||||
import { toSync } from '@src/lib/utils'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
import type { modelingMachine } from '@src/machines/modelingMachine'
 | 
			
		||||
 | 
			
		||||
const semanticEntityNames: {
 | 
			
		||||
 | 
			
		||||
@ -8,10 +8,7 @@ import {
 | 
			
		||||
  getSelectionCountByType,
 | 
			
		||||
} from '@src/lib/selections'
 | 
			
		||||
import { kclManager } from '@src/lib/singletons'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
const selectionSelector = (snapshot: any) => snapshot?.context.selectionRanges
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,7 @@ import { useEffect, useRef } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
import type { CommandArgument } from '@src/lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  commandBarActor,
 | 
			
		||||
  useCommandBarState,
 | 
			
		||||
} from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
function CommandBarTextareaInput({
 | 
			
		||||
  arg,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
 | 
			
		||||
import usePlatform from '@src/hooks/usePlatform'
 | 
			
		||||
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
import { CustomIcon } from '@src/components/CustomIcon'
 | 
			
		||||
 | 
			
		||||
export function CommandBarOpenButton() {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { CustomIcon } from '@src/components/CustomIcon'
 | 
			
		||||
import type { Command } from '@src/lib/commandTypes'
 | 
			
		||||
import { sortCommands } from '@src/lib/commandUtils'
 | 
			
		||||
import { getActorNextEvents } from '@src/lib/utils'
 | 
			
		||||
import { commandBarActor } from '@src/machines/commandBarMachine'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { useState } from 'react'
 | 
			
		||||
 | 
			
		||||
import { ActionButton } from '@src/components/ActionButton'
 | 
			
		||||
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
 | 
			
		||||
import { useSettings } from '@src/machines/appMachine'
 | 
			
		||||
import { useSettings } from '@src/lib/singletons'
 | 
			
		||||
import { useSearchParams } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
const DownloadAppBanner = () => {
 | 
			
		||||
 | 
			
		||||