merge main

This commit is contained in:
benjamaan476
2025-04-25 15:31:54 +01:00
177 changed files with 34778 additions and 6264 deletions

View File

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

1
.husky/pre-commit Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@ -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": {

View File

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

View File

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

View File

@ -8,10 +8,7 @@ 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'] },
() => {
test.describe('Can create sketches on all planes and their back sides', () => {
const sketchOnPlaneAndBackSideTest = async (
page: Page,
homePage: HomePageFixture,
@ -133,5 +130,4 @@ test.describe(
)
})
}
}
)
})

View File

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

View File

@ -9,7 +9,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({
page,
homePage,
@ -179,10 +179,10 @@ 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 }) => {
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()
@ -217,9 +217,10 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
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 expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
@ -228,8 +229,7 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
).toBeVisible()
// Check that the theme changed
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
}
)
})
test('Can extrude from the command bar', async ({
page,

View File

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

View File

@ -10,7 +10,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
test.describe('Editor tests', () => {
test('can comment out code with ctrl+/', async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
@ -989,10 +989,11 @@ sketch001 = startSketchOn(XZ)
|> close()`)
})
test(
'Can undo a sketch modification with ctrl+z',
{ tag: ['@skipWin'] },
async ({ page, homePage, editor }) => {
test('Can undo a sketch modification with ctrl+z', async ({
page,
homePage,
editor,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
@ -1143,8 +1144,7 @@ sketch001 = startSketchOn(XZ)
|> extrude(length = 5)`,
{ shouldNormalise: true }
)
}
)
})
test(
`Can import a local OBJ file`,

View File

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

View File

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

View File

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

View File

@ -6,7 +6,10 @@ import { expect, test } from '@e2e/playwright/zoo-test'
* Not all menu actions are tested. Some are default electron menu actions.
* Test file menu actions that trigger something in the frontend
*/
test.describe('Native file menu', { tag: ['@electron'] }, () => {
test.describe(
'Native file menu',
{ tag: ['@electron', '@macos', '@windows'] },
() => {
test.skip() // TODO: Reimplement native file menu tests
test.describe('Home page', () => {
test.describe('File role', () => {
@ -161,7 +164,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
const defaultUnit = settings.locator('#defaultUnit')
await expect(defaultUnit).toBeVisible()
})
test('Home.File.Preferences.Theme', async ({ tronApp, cmdBar, page }) => {
test('Home.File.Preferences.Theme', async ({
tronApp,
cmdBar,
page,
}) => {
if (!tronApp) fail()
// Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -365,7 +372,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
})
})
test.describe('Help role', () => {
test('Home.Help.Show all commands', async ({ tronApp, cmdBar, page }) => {
test('Home.Help.Show all commands', async ({
tronApp,
cmdBar,
page,
}) => {
if (!tronApp) fail()
// Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -389,7 +400,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
const actual = cmdBar.cmdBarElement.getByTestId('cmd-bar-search')
await expect(actual).toBeVisible()
})
test('Home.Help.KCL code samples', async ({ tronApp, cmdBar, page }) => {
test('Home.Help.KCL code samples', async ({
tronApp,
cmdBar,
page,
}) => {
if (!tronApp) fail()
// Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -435,7 +450,11 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
)
await expect(actual).toBeVisible()
})
test('Home.Help.Reset onboarding', async ({ tronApp, cmdBar, page }) => {
test('Home.Help.Reset onboarding', async ({
tronApp,
cmdBar,
page,
}) => {
if (!tronApp) fail()
// Run electron snippet to find the Menu!
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
@ -2354,4 +2373,5 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
})
})
})
})
}
)

View File

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

View File

@ -18,7 +18,7 @@ import { expect, test } from '@e2e/playwright/zoo-test'
test(
'projects reload if a new one is created, deleted, or renamed externally',
{ tag: '@electron' },
{ tag: ['@electron', '@macos', '@windows'] },
async ({ context, page }, testInfo) => {
let externalCreatedProjectName = 'external-created-project'

View File

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

View File

@ -14,7 +14,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
test.describe('Regression tests', () => {
// bugs we found that don't fit neatly into other categories
test('bad model has inline error #3251', async ({
context,
@ -239,10 +239,11 @@ 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 }) => {
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 })
@ -315,8 +316,7 @@ extrude001 = extrude(sketch001, length = 50)
thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
}
)
})
test(
'window resize updates should reconfigure the stream',
@ -486,10 +486,10 @@ 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 }) => {
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(
@ -560,8 +560,7 @@ extrude002 = extrude(profile002, length = 150)
await expect(successToastMessage).toHaveCount(2)
})
}
)
})
test(
`Network health indicator only appears in modeling view`,

View File

@ -16,7 +16,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test.describe('Sketch tests', () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
page,
context,
@ -393,10 +393,13 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|> close()
`)
}
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene, toolbar, cmdBar }) => {
test('code pane open at start-handles', async ({
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -417,13 +420,15 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
toolbar,
cmdBar
)
}
)
})
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage, scene, toolbar, cmdBar }) => {
test('code pane closed at start-handles', async ({
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -439,8 +444,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
toolbar,
cmdBar
)
}
)
})
})
test('Can edit a circle center and radius by dragging its handles', async ({
@ -1389,10 +1393,15 @@ offsetPlane001 = offsetPlane(XY, offset = 10)`
})
test.describe('multi-profile sketching', () => {
test(
`test it removes half-finished expressions when changing tools in sketch mode`,
{ tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
test(`test it removes half-finished expressions when changing tools in sketch mode`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
test.fixme(orRunWhenFullSuiteEnabled())
// We seed the scene with a single offset plane
await context.addInitScript(() => {
@ -1461,9 +1470,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await test.step('equip line tool and verify three-point circle code is removed', async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.not.toContain(
'profile003 = circleThreePoint('
)
await editor.expectEditor.not.toContain('profile003 = circleThreePoint(')
})
await test.step('equip three-point-arc tool and click first two points', async () => {
@ -1520,12 +1527,16 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
await editor.expectEditor.not.toContain('arc(')
await editor.expectEditor.toContain('profile002')
})
}
)
test(
`snapToProfile start only works for current profile`,
{ tag: ['@skipWin'] },
async ({ context, page, scene, toolbar, editor, homePage, cmdBar }) => {
})
test(`snapToProfile start only works for current profile`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
cmdBar,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
@ -1564,7 +1575,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
const codeFromTangentialArc = ` |> tangentialArc(endAbsolute = [39.49, 88.22])`
await test.step('check that tangential tool does not snap to other profile starts', async () => {
await toolbar.tangentialArcBtn.click()
await toolbar.selectTangentialArc()
await page.waitForTimeout(1000)
await endOfLowerSegMove()
await page.waitForTimeout(1000)
@ -1604,8 +1615,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
`[profileStartX(%), profileStartY(%)]`
)
})
}
)
})
test('can enter sketch mode for sketch with no profiles', async ({
scene,
toolbar,
@ -1777,7 +1787,7 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
await endLineStartTanArc()
await editor.expectEditor.toContain(`|> line(end = [9.02, -0.55])`)
await toolbar.tangentialArcBtn.click()
await toolbar.selectTangentialArc()
await page.waitForTimeout(300)
await page.mouse.click(745, 359)
await page.waitForTimeout(300)
@ -2071,10 +2081,13 @@ profile003 = startProfileAt([206.63, -56.73], sketch001)
})
})
test(
'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
test('Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2204,12 +2217,15 @@ profile004 = circleThreePoint(sketch001, p1 = [13.44, -6.8], p2 = [13.39, -2.07]
|> close()`.replaceAll('\n', '')
)
})
}
)
test(
'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added',
{ tag: ['@skipWin', '@skipLinux'] },
async ({ scene, toolbar, editor, cmdBar, page, homePage }) => {
})
test('Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added', async ({
scene,
toolbar,
editor,
cmdBar,
page,
homePage,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2303,13 +2319,15 @@ profile003 = circle(sketch001, center = [6.92, -4.2], radius = 3.16)
await editor.expectEditor.not.toContain('length001 = 7')
await sketchIsDrawnProperly()
})
}
)
})
test(
'can enter sketch when there is an extrude',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, page, cmdBar }) => {
test('can enter sketch when there is an extrude', async ({
homePage,
scene,
toolbar,
page,
cmdBar,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2361,8 +2379,7 @@ extrude001 = extrude(profile003, length = 5)
scene.expectPixelColor(TEST_COLORS.WHITE, { x: 763, y: 214 }, 15),
])
})
}
)
})
test('exit new sketch without drawing anything should not be a problem', async ({
homePage,
scene,
@ -2416,10 +2433,13 @@ extrude001 = extrude(profile003, length = 5)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
})
})
test(
'A sketch with only "startProfileAt" and no segments should still be able to be continued',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
test('A sketch with only "startProfileAt" and no segments should still be able to be continued', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2457,12 +2477,14 @@ profile002 = startProfileAt([85.81, 52.55], sketch002)
await page.waitForTimeout(100)
await nextPoint()
await editor.expectEditor.toContain(`|> line(end = [126.05, 44.12])`)
}
)
test(
'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
})
test('old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2517,12 +2539,15 @@ extrude001 = extrude(thePart, length = 75)
await profilePoint2()
await editor.expectEditor.toContain(`|> line(end = [18.97, -18.06])`)
})
}
)
test(
'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page, cmdBar }) => {
})
test('Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections', async ({
homePage,
scene,
toolbar,
editor,
page,
cmdBar,
}) => {
// TODO this test should include a test for selecting revolve walls and caps
await page.addInitScript(async () => {
@ -2681,12 +2706,14 @@ extrude003 = extrude(profile011, length = 2.5)
})
}
}) */
}
)
test(
'Can enter sketch loft edges, base and continue sketch',
{ tag: ['@skipWin'] },
async ({ homePage, scene, toolbar, editor, page }) => {
})
test('Can enter sketch loft edges, base and continue sketch', async ({
homePage,
scene,
toolbar,
editor,
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
@ -2736,8 +2763,7 @@ loft([profile001, profile002])
await editor.expectEditor.toContain(
`angledLine(angle = 0, length = 113.01, tag = $rectangleSegmentA001)`
)
}
)
})
test('Can enter sketch loft edges offsetPlane and continue sketch', async ({
scene,
toolbar,
@ -2970,7 +2996,7 @@ test.describe('Redirecting to home page and back to the original file should cle
await click00r(200, -200)
// Draw arc
await toolbar.tangentialArcBtn.click()
await toolbar.selectTangentialArc()
await click00r(0, 0)
await click00r(100, 100)
@ -3245,10 +3271,15 @@ profile003 = startProfileAt([-201.08, 254.17], sketch002)
).toBeVisible()
})
})
test(
'adding a syntax error, recovers after fixing',
{ tag: ['@skipWin'] },
async ({ page, homePage, context, scene, editor, toolbar, cmdBar }) => {
test('adding a syntax error, recovers after fixing', async ({
page,
homePage,
context,
scene,
editor,
toolbar,
cmdBar,
}) => {
const file = await fs.readFile(
path.resolve(
__dirname,
@ -3335,6 +3366,5 @@ profile003 = startProfileAt([-201.08, 254.17], sketch002)
15
)
})
}
)
})
})

View File

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

View File

@ -8,7 +8,12 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Test network and connection issues', () => {
test.describe(
'Test network and connection issues',
{
tag: ['@macos', '@windows'],
},
() => {
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
@ -184,7 +189,9 @@ test.describe('Test network and connection issues', () => {
await toolbar.editSketch()
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page
.getByRole('button', { name: 'line Line', exact: true })
.click()
await page.waitForTimeout(150)
@ -254,4 +261,5 @@ profile001 = startProfileAt([12.34, -12.34], sketch001)
).not.toBeVisible()
}
)
})
}
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,12 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Testing settings', () => {
test.describe(
'Testing settings',
{
tag: ['@macos', '@windows'],
},
() => {
test('Stored settings are validated and fall back to defaults', async ({
page,
homePage,
@ -93,11 +98,14 @@ test.describe('Testing settings', () => {
/** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
await test.step(`Confirm that this dialog has a solid background`, async () => {
await expect
.poll(() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]), {
.poll(
() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]),
{
timeout: 1000,
message:
'Checking for solid background, should not see default plane colors',
})
}
)
.toBeLessThan(15)
})
@ -130,7 +138,9 @@ test.describe('Testing settings', () => {
// Roll back to default of "off"
await await page
.getByText('show debug panelRoll back show debug panelRoll back to match')
.getByText(
'show debug panelRoll back show debug panelRoll back to match'
)
.hover()
await page
.getByRole('button', {
@ -180,7 +190,10 @@ test.describe('Testing settings', () => {
await expect(hotkey).toHaveText(text)
})
test('Project and user settings can be reset', async ({ page, homePage }) => {
test('Project and user settings can be reset', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -268,7 +281,7 @@ test.describe('Testing settings', () => {
test(
`Project settings override user settings on desktop`,
{ tag: ['@electron', '@skipWin'] },
{ tag: ['@electron'] },
async ({ context, page }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled())
const projectName = 'bracket'
@ -291,13 +304,18 @@ test.describe('Testing settings', () => {
projectName,
PROJECT_SETTINGS_FILE_NAME
)
const tempUserSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
const tempUserSettingsFilePath = join(
projectDirName,
SETTINGS_FILE_NAME
)
const userThemeColor = '120'
const projectThemeColor = '50'
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
const themeColorSetting = page
.locator('#themeColor')
.getByRole('slider')
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
const userSettingsTab = page.getByRole('radio', { name: 'User' })
const settingsCloseButton = page.getByTestId('settings-close-button')
@ -532,7 +550,10 @@ test.describe('Testing settings', () => {
join(bracketDir, '2.kcl')
)
})
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
const kclCube = await fsp.readFile(
executorInputPath('cube.kcl'),
'utf-8'
)
const kclCylinder = await fsp.readFile(
executorInputPath('cylinder.kcl'),
'utf8'
@ -586,7 +607,9 @@ test.describe('Testing settings', () => {
test.skip(process.platform === 'win32', 'Skip on windows')
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
const toastMessage = page.getByText(`Successfully created "testDefault"`)
const toastMessage = page.getByText(
`Successfully created "testDefault"`
)
await expect(toastMessage).not.toBeVisible()
await page
.getByRole('button', { name: 'Start Sketch' })
@ -620,7 +643,9 @@ test.describe('Testing settings', () => {
})
await test.step('Change modeling default unit within project tab', async () => {
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
const changeUnitOfMeasureInProjectTab = async (
unitOfMeasure: string
) => {
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
await page
.getByTestId('modeling-defaultUnit')
@ -674,7 +699,9 @@ test.describe('Testing settings', () => {
await test.step('Change modeling default unit within command bar', async () => {
const commands = page.getByRole('button', { name: 'Commands' })
const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => {
const changeUnitOfMeasureInCommandBar = async (
unitOfMeasure: string
) => {
// Open command bar
await commands.click()
const settingsModelingDefaultUnitCommand = page.getByText(
@ -885,7 +912,10 @@ test.describe('Testing settings', () => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
localStorage.setItem(
'persistModelingContext',
'{"openPanes":["debug"]}'
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -1057,4 +1087,5 @@ fn cube`
}
)
})
})
}
)

View File

@ -9,7 +9,7 @@ import {
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page, homePage }) => {
const u = await getUtils(page)
@ -436,10 +436,10 @@ 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('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)
@ -521,8 +521,7 @@ test.describe('Text-to-CAD tests', { tag: ['@skipWin'] }, () => {
// Expect the code to be pasted.
const code2x4 = await page.locator('.cm-content').innerText()
await expect(code2x4.length).toBeGreaterThan(249)
}
)
})
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
page,

View File

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

1
interface.d.ts vendored
View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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 {

View File

@ -459,7 +459,7 @@ impl ExecutorContext {
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let format = super::import::format_from_annotations(attrs, path, source_range)?;
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None));
Ok(id)
}
ImportPath::Std { .. } => {
@ -501,7 +501,7 @@ impl ExecutorContext {
*cache = Some((val, er, items.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom) => Err(KclError::Semantic(KclErrorDetails {
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
message: "Cannot import items from foreign modules".to_owned(),
source_ranges: vec![geom.source_range],
})),
@ -546,9 +546,20 @@ impl ExecutorContext {
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(geom) => super::import::send_to_engine(geom.clone(), self)
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))),
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result {
Ok(val) => {
*cached = val.clone();
Ok(val)
}
Err(e) => Err(e),
}
}
ModuleRepr::Dummy => unreachable!(),
};

View File

@ -752,7 +752,12 @@ 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)
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
@ -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,14 +859,13 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
@ -852,14 +881,13 @@ impl ExecutorContext {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_ctxt
.exec_module_from_ast(
&program,
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
false,
)
.await;
@ -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;
match &mut repr {
ModuleRepr::Kcl(_, cache) => {
let ModuleRepr::Kcl(_, session_data) = new_repr else {
unreachable!();
};
*cache = Some((val, session_data, variables));
*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);
}

View File

@ -28,6 +28,10 @@ pub enum RuntimeType {
}
impl RuntimeType {
pub fn edge() -> Self {
RuntimeType::Primitive(PrimitiveType::Edge)
}
pub fn sketch() -> Self {
RuntimeType::Primitive(PrimitiveType::Sketch)
}
@ -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);
}
}

View File

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

View File

@ -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";

View File

@ -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)
}

View File

@ -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;
}
}

View File

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

View File

@ -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,18 +118,19 @@ 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 {
// 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()
@ -141,6 +142,19 @@ pub(crate) fn import_dependencies(
})?
.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
}
}
}
for child in node.children().iter() {
@ -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),
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,3 @@
import "../inputs/cube.step" as cube
clone(cube)

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,3 @@
```mermaid
flowchart LR
```

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

View File

@ -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
)

View File

@ -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"
}
]

View File

@ -0,0 +1,3 @@
import "../inputs/cube.step" as cube
clone(cube)

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -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)

View File

@ -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)

View File

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

59
scripts/diff.js Normal file
View 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}`)
})

View File

@ -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'

View File

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

View File

@ -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 />,
},
],

View File

@ -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 = '',

View File

@ -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 } {

View File

@ -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,

View File

@ -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'

View File

@ -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()

View File

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

View File

@ -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'

View File

@ -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()

View File

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

View File

@ -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()

View File

@ -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'

View File

@ -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'

View File

@ -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()

View File

@ -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: {

View File

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

View File

@ -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,

View File

@ -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() {

View File

@ -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,

View File

@ -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 = () => {

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