Compare commits

..

6 Commits

279 changed files with 2802 additions and 120760 deletions

View File

@ -1,3 +1,3 @@
[codespell] [codespell]
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts,./packages/codemirror-lang-kcl/test/all.test.ts

View File

@ -5,24 +5,16 @@
}, },
"plugins": [ "plugins": [
"css-modules", "css-modules",
"jest",
"react",
"suggest-no-throw", "suggest-no-throw",
"@typescript-eslint"
], ],
"extends": [ "extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended" "plugin:css-modules/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-misused-promises": "error",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"semi": [ "semi": [
"error", "error",
"never" "never"

View File

@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
fi fi
retry=1 retry=1
max_retrys=5 max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do while [[ $retry -le $max_retrys ]]; do

View File

@ -5,28 +5,24 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: 'npm' # See documentation for possible values - package-ecosystem: 'npm' # See documentation for possible values
directory: '/' # Location of package manifests directory: '/' # Location of package manifests
schedule: schedule:
interval: 'weekly' interval: 'weekly'
reviewers: reviewers:
- franknoirot - franknoirot
- irev-dev - irev-dev
- package-ecosystem: 'github-actions' # See documentation for possible values - package-ecosystem: 'github-actions' # See documentation for possible values
directory: '/' # Location of package manifests directory: '/' # Location of package manifests
schedule: schedule:
interval: 'weekly' interval: 'weekly'
reviewers: reviewers:
- adamchalmers - adamchalmers
- jessfraz - jessfraz
- package-ecosystem: 'cargo' # See documentation for possible values - package-ecosystem: 'cargo' # See documentation for possible values
directory: '/src/wasm-lib/' # Location of package manifests directory: '/src/wasm-lib/' # Location of package manifests
schedule: schedule:
interval: 'weekly' interval: 'weekly'
reviewers: reviewers:
- adamchalmers - adamchalmers
- jessfraz - jessfraz
groups:
serde-dependencies:
patterns:
- "serde*"

View File

@ -1,7 +1,7 @@
name: E2E Tests name: E2E Tests
on: on:
push: push:
branches: [ main ] branches: [ main, pierremtb/e2e-snapshots-linux-only-test-bot-update ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]

View File

@ -337,47 +337,13 @@ For individual testing:
yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
``` ```
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default. Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default.
### Rust tests ### Rust tests
**Dependencies**
- `KITTYCAD_API_TOKEN`
- `cargo-nextest`
- `just`
#### Setting KITTYCAD_API_TOKEN
Use the production zoo.dev token, set this environment variable before running the tests
#### Installing cargonextest
```
cd src/wasm-lib
cargo search cargo-nextest
cargo install cargo-nextest
```
#### just
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
#### Running the tests
```bash ```bash
# With just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
# Make sure you installed just
cd src/wasm-lib cd src/wasm-lib
just test KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
```
```bash
# Without just
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
# Make sure you installed cargo-nextest
cd src/wasm-lib
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
``` ```
Where `XXX` is an API token from the production engine (NOT the dev environment). Where `XXX` is an API token from the production engine (NOT the dev environment).

View File

@ -24,5 +24,3 @@ once fixed in engine will just start working here with no language changes.
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work. - **Appearance**: Changing the appearance on a loft does not work.
- **Helix**: Currently sweeping a helix does not work.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,6 @@ layout: manual
* [`getOppositeEdge`](kcl/getOppositeEdge) * [`getOppositeEdge`](kcl/getOppositeEdge)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix) * [`helix`](kcl/helix)
* [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`import`](kcl/import) * [`import`](kcl/import)
@ -82,7 +81,6 @@ layout: manual
* [`pi`](kcl/pi) * [`pi`](kcl/pi)
* [`polar`](kcl/polar) * [`polar`](kcl/polar)
* [`polygon`](kcl/polygon) * [`polygon`](kcl/polygon)
* [`pop`](kcl/pop)
* [`pow`](kcl/pow) * [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart) * [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX) * [`profileStartX`](kcl/profileStartX)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,42 +0,0 @@
---
title: "Axis3dOrEdgeReference"
excerpt: "A 3D axis or tagged edge."
layout: manual
---
A 3D axis or tagged edge.
**This schema accepts any of the following:**
3D axis and origin.
[`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d)
----
Tagged edge.
[`EdgeReference`](/docs/kcl/types/EdgeReference)
----

View File

@ -1,10 +1,10 @@
--- ---
title: "AxisAndOrigin2d" title: "AxisAndOrigin"
excerpt: "A 2D axis and origin." excerpt: "Axis and origin."
layout: manual layout: manual
--- ---
A 2D axis and origin. Axis and origin.

View File

@ -1,105 +0,0 @@
---
title: "AxisAndOrigin3d"
excerpt: "A 3D axis and origin."
layout: manual
---
A 3D axis and origin.
**This schema accepts exactly one of the following:**
X-axis.
**enum:** `X`
----
Y-axis.
**enum:** `Y`
----
Z-axis.
**enum:** `Z`
----
Flip the X-axis.
**enum:** `-X`
----
Flip the Y-axis.
**enum:** `-Y`
----
Flip the Z-axis.
**enum:** `-Z`
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `custom` |`object`| | No |
----

View File

@ -1,19 +1,19 @@
--- ---
title: "Axis2dOrEdgeReference" title: "AxisOrEdgeReference"
excerpt: "A 2D axis or tagged edge." excerpt: "Axis or tagged edge."
layout: manual layout: manual
--- ---
A 2D axis or tagged edge. Axis or tagged edge.
**This schema accepts any of the following:** **This schema accepts any of the following:**
2D axis and origin. Axis and origin.
[`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d) [`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin)

View File

@ -1,25 +0,0 @@
---
title: "Helix"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,10 +1,10 @@
--- ---
title: "HelixData" title: "HelixData"
excerpt: "Data for a helix." excerpt: "Data for helices."
layout: manual layout: manual
--- ---
Data for a helix. Data for helices.
**Type:** `object` **Type:** `object`
@ -19,8 +19,6 @@ Data for a helix.
| `revolutions` |`number`| Number of revolutions. | No | | `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No | | `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No | | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. | No | | `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
| `radius` |`number`| Radius of the helix. | No |
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -1,24 +0,0 @@
---
title: "HelixRevolutionsData"
excerpt: "Data for helix revolutions."
layout: manual
---
Data for helix revolutions.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |

View File

@ -1,25 +0,0 @@
---
title: "HelixValue"
excerpt: "A helix."
layout: manual
---
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -285,27 +285,6 @@ An solid is a collection of extrude surfaces.
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No | | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
----
A helix.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
| `value` |`string`| The id of the helix. | No |
| `revolutions` |`number`| Number of revolutions. | No |
| `angleStart` |`number`| Start angle (in degrees). | No |
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
---- ----
Data for an imported geometry. Data for an imported geometry.

View File

@ -16,6 +16,6 @@ Data for a mirror.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No | | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No |

View File

@ -17,7 +17,7 @@ Data for revolution surfaces.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No |
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No | | `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No |
| `tolerance` |`number`| Tolerance for the revolve operation. | No | | `tolerance` |`number`| Tolerance for the revolve operation. | No |

View File

@ -16,7 +16,7 @@ Data for a sweep.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No | | `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No | | `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No | | `tolerance` |`number`| Tolerance for the sweep operation. | No |

View File

@ -1,42 +0,0 @@
---
title: "SweepPath"
excerpt: "A path to sweep along."
layout: manual
---
A path to sweep along.
**This schema accepts any of the following:**
A path to sweep along.
[`Sketch`](/docs/kcl/types/Sketch)
----
A path to sweep along.
[`Helix`](/docs/kcl/types/Helix)
----

View File

@ -36,8 +36,7 @@ type DragFromHandler = (
export class SceneFixture { export class SceneFixture {
public page: Page public page: Page
public streamWrapper!: Locator
public loadingIndicator!: Locator
private exeIndicator!: Locator private exeIndicator!: Locator
constructor(page: Page) { constructor(page: Page) {
@ -65,8 +64,6 @@ export class SceneFixture {
this.page = page this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.streamWrapper = page.getByTestId('stream')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
} }
makeMouseHelpers = ( makeMouseHelpers = (

View File

@ -14,7 +14,6 @@ export class ToolbarFixture {
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator shellButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
@ -41,7 +40,6 @@ export class ToolbarFixture {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell') this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')

View File

@ -756,17 +756,6 @@ test(`Offset plane point-and-click`, async ({
}) })
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation(
'Offset Plane',
0
)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
}) })
const loftPointAndClickCases = [ const loftPointAndClickCases = [
@ -862,173 +851,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
}) })
await scene.expectPixelColor([89, 89, 89], testPoint, 15) await scene.expectPixelColor([89, 89, 89], testPoint, 15)
}) })
await test.step('Delete loft via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
})
// TODO: merge with above test. Right now we're not able to delete a loft
// right after creation via selection for some reason, so we go with a new instance
test('Loft and offset plane deletion via selection', async ({
context,
page,
homePage,
scene,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002])
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
await test.step(`Delete loft`, async () => {
// Check for loft
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
await clickOnSketch1()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 30 }, %)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
await test.step('Delete sketch002', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
|> circle({ center = [0, 0], radius = 20 }, %)
`)
await page.keyboard.press('Backspace')
// Check for plane001
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
})
await test.step('Delete plane001', async () => {
await page.waitForTimeout(1000)
await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50)
`)
await page.keyboard.press('Backspace')
// Check for sketch 1
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
})
test(`Sweep point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> tangentialArcTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'profile',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '',
},
highlightedHeaderArg: 'profile',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '1 face',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [sweepDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
})
await test.step('Delete sweep via feature tree selection', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
}) })
}) })
@ -1208,104 +1030,4 @@ extrude001 = extrude(40, sketch001)
}) })
await scene.expectPixelColor([49, 49, 49], testPoint, 15) await scene.expectPixelColor([49, 49, 49], testPoint, 15)
}) })
await test.step('Delete shell via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
})
const shellSketchOnFacesCases = [
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
|> extrude(100, %)
sketch002 = startSketchOn(sketch001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
|> extrude(50, %)
`,
`sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 100 }, %)
extrude001 = extrude(100, sketch001)
sketch002 = startSketchOn(extrude001, 'END')
|> circle({ center = [0, 0], radius = 50 }, %)
extrude002 = extrude(50, sketch002)
`,
]
shellSketchOnFacesCases.forEach((initialCode, index) => {
const hasExtrudesInPipe = index === 0
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
})`
await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await toolbar.openPane('code')
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
})
})
}) })

View File

@ -115,7 +115,7 @@ test(
) )
test( test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene', 'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -199,7 +199,7 @@ test(
) )
test( test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene', 'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -276,7 +276,7 @@ test(
) )
test( test(
'open a file in a project works and renders, open empty file, it should clear the scene', 'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
@ -1885,48 +1885,3 @@ test.fixme(
}) })
} }
) )
test(
'project name with foreign characters should open',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
path.join(bracketDir, 'main.kcl')
)
await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible()
await page.getByText('اَلْعَرَبِيَّةُ').click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
}
)

View File

@ -614,38 +614,6 @@ extrude001 = extrude(50, sketch001)
await expect(gizmo).toBeVisible() await expect(gizmo).toBeVisible()
}) })
}) })
test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({
context,
homePage,
scene,
toolbar,
viewport,
}) => {
await context.folderSetupFn(async (dir) => {
const legoDir = path.join(dir, 'lego')
await fsp.mkdir(legoDir, { recursive: true })
await fsp.copyFile(
executorInputPath('lego.kcl'),
path.join(legoDir, 'main.kcl')
)
})
await test.step(`Test setup`, async () => {
await homePage.openProject('lego')
await toolbar.closePane('code')
})
await test.step(`Waiting for the loading spinner to disappear`, async () => {
await scene.loadingIndicator.waitFor({ state: 'detached' })
})
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
await scene.expectPixelColor(
[143, 143, 143],
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
15
)
})
})
}) })
async function clickExportButton(page: Page) { async function clickExportButton(page: Page) {

View File

@ -39,8 +39,8 @@ test.describe('Sketch tests', () => {
${startProfileAt1} ${startProfileAt1}
|> arc({ |> arc({
radius = screwRadius, radius = screwRadius,
angleStart = 0, angle_start = 0,
angleEnd = 360 angle_end = 360
}, %) }, %)
part001 = startSketchOn('XY') part001 = startSketchOn('XY')
@ -60,8 +60,8 @@ test.describe('Sketch tests', () => {
|> yLine(wireOffset, %) |> yLine(wireOffset, %)
|> arc({ |> arc({
radius = wireRadius, radius = wireRadius,
angleStart = 0, angle_start = 0,
angleEnd = 180 angle_end = 180
}, %) }, %)
|> yLine(-wireOffset, %) |> yLine(-wireOffset, %)
|> xLine(-width / 4, %) |> xLine(-width / 4, %)
@ -1323,85 +1323,3 @@ test.describe(`Sketching with offset planes`, () => {
}) })
}) })
}) })
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4891
test.describe(`Click based selection don't brick the app when clicked out of range after format using cache`, () => {
test(`Can select a line that reformmed after entering sketch mode`, async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
`sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> line([3.14, 3.14], %)
|> arcTo({
end = [4, 2],
interior = [1, 2]
}, %)
`
)
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await test.step(`format the code`, async () => {
// doesn't contain condensed version
await editor.expectEditor.not.toContain(
`arcTo({ end = [4, 2], interior = [1, 2] }, %)`
)
// click the code to enter sketch mode
await page.getByText(`arcTo`).click()
// Format the code.
await page.locator('#code-pane button:first-child').click()
await page.locator('button:has-text("Format code")').click()
})
await test.step(`Ensure the code reformatted`, async () => {
await editor.expectEditor.toContain(
`arcTo({ end = [4, 2], interior = [1, 2] }, %)`
)
})
const [arcClick, arcHover] = scene.makeMouseHelpers(699, 337)
await test.step('Ensure we can hover the arc', async () => {
await arcHover()
// Check that the code is highlighted
await editor.expectState({
activeLines: ["sketch001=startSketchOn('XZ')"],
diagnostics: [],
highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)',
})
})
await test.step('reset the selection', async () => {
// Move the mouse out of the way
await page.mouse.move(655, 337)
await editor.expectState({
activeLines: ["sketch001=startSketchOn('XZ')"],
diagnostics: [],
highlightedCode: '',
})
})
await test.step('Ensure we can click the arc', async () => {
await arcClick()
// Check that the code is highlighted
await editor.expectState({
activeLines: [],
diagnostics: [],
highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)',
})
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -389,25 +389,25 @@ test.describe('Testing selections', () => {
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
plane = { plane = {
origin = { x = 0, y = -50, z = 0 }, origin = { x = 0, y = -50, z = 0 },
xAxis = { x = 1, y = 0, z = 0 }, x_axis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 }, y_axis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = -1, z = 0 } z_axis = { x = 0, y = -1, z = 0 }
} }
})`) })`)
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
plane = { plane = {
origin = { x = 116.53, y = 0, z = 163.25 }, origin = { x = 116.53, y = 0, z = 163.25 },
xAxis = { x = -0.81, y = 0, z = 0.58 }, x_axis = { x = -0.81, y = 0, z = 0.58 },
yAxis = { x = 0, y = -1, z = 0 }, y_axis = { x = 0, y = -1, z = 0 },
zAxis = { x = 0.58, y = 0, z = 0.81 } z_axis = { x = 0.58, y = 0, z = 0.81 }
} }
})`) })`)
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
plane = { plane = {
origin = { x = -91.74, y = 0, z = 80.89 }, origin = { x = -91.74, y = 0, z = 80.89 },
xAxis = { x = -0.66, y = 0, z = -0.75 }, x_axis = { x = -0.66, y = 0, z = -0.75 },
yAxis = { x = 0, y = -1, z = 0 }, y_axis = { x = 0, y = -1, z = 0 },
zAxis = { x = -0.75, y = 0, z = 0.66 } z_axis = { x = -0.75, y = 0, z = 0.66 }
} }
})`) })`)

View File

@ -156,13 +156,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' }) const textToCadCommand = page.getByText('Text-to-CAD')
await expect(textToCadCommand.first()).toBeVisible() await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command // Click the Text-to-CAD command
await textToCadCommand.first().click() await textToCadCommand.first().click()
// Enter the prompt. // Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' }) const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
// Type the prompt. // Type the prompt.
@ -224,13 +224,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' }) const textToCadCommand = page.getByText('Text-to-CAD')
await expect(textToCadCommand.first()).toBeVisible() await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command // Click the Text-to-CAD command
await textToCadCommand.first().click() await textToCadCommand.first().click()
// Enter the prompt. // Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' }) const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss' const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
@ -314,13 +314,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' }) const textToCadCommand = page.getByText('Text-to-CAD')
await expect(textToCadCommand.first()).toBeVisible() await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command // Click the Text-to-CAD command
await textToCadCommand.first().click() await textToCadCommand.first().click()
// Enter the prompt. // Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' }) const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
const badPrompt = 'akjsndladflajbhflauweyf15;' const badPrompt = 'akjsndladflajbhflauweyf15;'
@ -392,13 +392,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands') const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' }) const textToCadCommand = page.getByText('Text-to-CAD')
await expect(textToCadCommand.first()).toBeVisible() await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command // Click the Text-to-CAD command
await textToCadCommand.first().click() await textToCadCommand.first().click()
// Enter the prompt. // Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' }) const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
// Type the prompt. // Type the prompt.
@ -604,7 +604,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// Enter the prompt. // Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' }) const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
// Type the prompt. // Type the prompt.

18
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1736320768, "lastModified": 1721933792,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1728538411, "lastModified": 1718428119,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -43,11 +43,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1736476219, "lastModified": 1721960387,
"narHash": "sha256-+qyv3QqdZCdZ3cSO/cbpEY6tntyYjfe1bB12mdpNFaY=", "narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "de30cc5963da22e9742bbbbb9a3344570ed237b9", "rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
"type": "github" "type": "github"
}, },
"original": { "original": {

8
interface.d.ts vendored
View File

@ -11,13 +11,6 @@ export interface IElectronAPI {
open: typeof dialog.showOpenDialog open: typeof dialog.showOpenDialog
save: typeof dialog.showSaveDialog save: typeof dialog.showSaveDialog
openExternal: typeof shell.openExternal openExternal: typeof shell.openExternal
takeElectronWindowScreenshot: ({
width,
height,
}: {
width: number
height: number
}) => Promise<string>
showInFolder: typeof shell.showItemInFolder showInFolder: typeof shell.showItemInFolder
/** Require to be called first before {@link loginWithDeviceFlow} */ /** Require to be called first before {@link loginWithDeviceFlow} */
startDeviceFlow: (host: string) => Promise<string> startDeviceFlow: (host: string) => Promise<string>
@ -93,6 +86,5 @@ export interface IElectronAPI {
declare global { declare global {
interface Window { interface Window {
electron: IElectronAPI electron: IElectronAPI
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
} }
} }

View File

@ -15,7 +15,7 @@
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0", "@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.3", "@codemirror/language": "^6.10.3",
"@codemirror/lint": "^6.8.4", "@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6", "@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0", "@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.13", "@kittycad/lib": "2.0.12",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1", "@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1", "@react-hook/resize-observer": "^2.0.1",
@ -52,13 +52,13 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.6.1", "react-hotkeys-hook": "^4.5.1",
"react-json-view": "^1.21.3", "react-json-view": "^1.21.3",
"react-modal": "^3.16.3", "react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2", "react-modal-promise": "^1.0.2",
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"sketch-helpers": "^0.0.4", "sketch-helpers": "^0.0.4",
"three": "^0.172.0", "three": "^0.166.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"uuid": "^11.0.2", "uuid": "^11.0.2",
"vscode-jsonrpc": "^8.2.1", "vscode-jsonrpc": "^8.2.1",
@ -91,8 +91,8 @@
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"", "remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings", "wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src", "lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src", "lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", "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:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -166,11 +166,13 @@
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/react-modal": "^3.16.3", "@types/react-modal": "^3.16.3",
"@types/three": "^0.172.0", "@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
@ -180,12 +182,11 @@
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-notarize": "1.2.2", "electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-suggest-no-throw": "^1.0.0", "eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^16.3.0", "happy-dom": "^15.11.7",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"husky": "^9.1.5", "husky": "^9.1.5",
"kill-port": "^2.0.1", "kill-port": "^2.0.1",
@ -199,7 +200,6 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6", "vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",

View File

@ -17,7 +17,7 @@
statement[@isGroup=Statement] { statement[@isGroup=Statement] {
ImportStatement { kw<"import"> ImportItems ImportFrom String } | ImportStatement { kw<"import"> ImportItems ImportFrom String } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } | FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression }
@ -85,7 +85,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens { @tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' } String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
Number { "." @digit+ | @digit+ ("." @digit+)? } Number { "." @digit+ | @digit+ ("." @digit*)? }
@precedence { Number, "." } @precedence { Number, "." }
AddOp { "+" | "-" } AddOp { "+" | "-" }

View File

@ -1,60 +0,0 @@
# full
fn two = () => {
return 2
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
Equals,
ParamList,
Arrow,
Body(ReturnStatement(return,
Number))))
# = is optional
fn one () => {
return 1
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
ParamList,
Arrow,
Body(ReturnStatement(return,
Number))))
# => is optional
fn one = () {
return 1
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
Equals,
ParamList,
Body(ReturnStatement(return,
Number))))
# terse
fn two() {
return 2
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
ParamList,
Body(ReturnStatement(return,
Number))))

View File

@ -1,43 +0,0 @@
# spaced
a = [0 .. 1]
==>
Program(VariableDeclaration(VariableDefinition,
Equals,
ArrayExpression(IntegerRange(Number,
Number))))
# compact
a = [0..1]
==>
Program(VariableDeclaration(VariableDefinition,
Equals,
ArrayExpression(IntegerRange(Number,
Number))))
# expr spaced
a = [start .. start + 10]
==>
Program(VariableDeclaration(VariableDefinition,
Equals,
ArrayExpression(IntegerRange(VariableName,
BinaryExpression(VariableName,
AddOp,
Number)))))
# expr compact
a = [start..start + 10]
==>
Program(VariableDeclaration(VariableDefinition,
Equals,
ArrayExpression(IntegerRange(VariableName,
BinaryExpression(VariableName,
AddOp,
Number)))))

View File

@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
// try to parse the content-length from the headers // try to parse the content-length from the headers
const length = parseInt(match[1]) const length = parseInt(match[1])
if (Number.isNaN(length)) if (isNaN(length))
return Promise.reject(new Error('invalid content length')) return Promise.reject(new Error('invalid content length'))
// slice the headers since we now have the content length // slice the headers since we now have the content length

View File

@ -368,20 +368,13 @@ export class LanguageServerPlugin implements PluginValue {
sortText, sortText,
filterText, filterText,
}) => { }) => {
const detailText = [
deprecated ? 'Deprecated' : undefined,
labelDetails ? labelDetails.detail : detail,
]
// Don't let undefined appear.
.filter(Boolean)
.join(' ')
const completion: Completion & { const completion: Completion & {
filterText: string filterText: string
sortText?: string sortText?: string
apply: string apply: string
} = { } = {
label, label,
detail: detailText, detail: labelDetails ? labelDetails.detail : detail,
apply: label, apply: label,
type: kind && CompletionItemKindMap[kind].toLowerCase(), type: kind && CompletionItemKindMap[kind].toLowerCase(),
sortText: sortText ?? label, sortText: sortText ?? label,
@ -389,11 +382,7 @@ export class LanguageServerPlugin implements PluginValue {
} }
if (documentation) { if (documentation) {
completion.info = () => { completion.info = () => {
const deprecatedHtml = deprecated const htmlString = formatMarkdownContents(documentation)
? '<p><strong>Deprecated</strong></p>'
: ''
const htmlString =
deprecatedHtml + formatMarkdownContents(documentation)
const htmlNode = document.createElement('div') const htmlNode = document.createElement('div')
htmlNode.style.display = 'contents' htmlNode.style.display = 'contents'
htmlNode.innerHTML = htmlString htmlNode.innerHTML = htmlString

View File

@ -32,9 +32,10 @@ export default defineConfig({
}, },
projects: [ projects: [
{ {
name: 'chromium', name: 'Google Chrome',
use: { use: {
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
channel: 'chrome',
contextOptions: { contextOptions: {
/* Chromium is the only one with these permission types */ /* Chromium is the only one with these permission types */
permissions: ['clipboard-write', 'clipboard-read'], permissions: ['clipboard-write', 'clipboard-read'],

View File

@ -75,7 +75,6 @@ function CommandBarTextareaInput({
target.selectionStart = selectionStart + 1 target.selectionStart = selectionStart + 1
target.selectionEnd = selectionStart + 1 target.selectionEnd = selectionStart + 1
} else if (event.key === 'Enter') { } else if (event.key === 'Enter') {
event.preventDefault()
formRef.current?.dispatchEvent( formRef.current?.dispatchEvent(
new Event('submit', { bubbles: true }) new Event('submit', { bubbles: true })
) )

View File

@ -157,38 +157,39 @@ export const ModelingMachineProvider = ({
'enable copilot': () => { 'enable copilot': () => {
editorManager.setCopilotEnabled(true) editorManager.setCopilotEnabled(true)
}, },
'sketch exit execute': ({ context: { store } }) => { // tsc reports this typing as perfectly fine, but eslint is complaining.
// TODO: Remove this async callback. For some reason eslint wouldn't // It's actually nonsensical, so I'm quieting.
// let me disable @typescript-eslint/no-misused-promises for the line. // eslint-disable-next-line @typescript-eslint/no-misused-promises
;(async () => { 'sketch exit execute': async ({
// When cancelling the sketch mode we should disable sketch mode within the engine. context: { store },
await engineCommandManager.sendSceneCommand({ }): Promise<void> => {
type: 'modeling_cmd_req', // When cancelling the sketch mode we should disable sketch mode within the engine.
cmd_id: uuidv4(), await engineCommandManager.sendSceneCommand({
cmd: { type: 'sketch_mode_disable' }, type: 'modeling_cmd_req',
}) cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') { if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine() await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
} }
sceneInfra.camControls.syncDirection = 'engineToClient' sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause() store.videoElement?.pause()
return kclManager return kclManager
.executeCode() .executeCode()
.then(() => { .then(() => {
if (engineCommandManager.engineConnection?.idleMode) return if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => { store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e) console.warn('Video playing was prevented', e)
})
}) })
.catch(reportRejection) })
})().catch(reportRejection) .catch(reportRejection)
}, },
'Set mouse state': assign(({ context, event }) => { 'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {} if (event.type !== 'Set mouse state') return {}
@ -270,7 +271,6 @@ export const ModelingMachineProvider = ({
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'default_camera_center_to_selection', type: 'default_camera_center_to_selection',
camera_movement: 'vantage',
}, },
}) })
.catch(reportRejection) .catch(reportRejection)

View File

@ -97,7 +97,6 @@ export const KclEditorPane = () => {
if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) { if (!editorIsMounted || !lastSelectionEvent || !editorManager.editorView) {
return return
} }
editorManager.editorView.dispatch({ editorManager.editorView.dispatch({
selection: lastSelectionEvent.codeMirrorSelection, selection: lastSelectionEvent.codeMirrorSelection,
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)], annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],

View File

@ -60,7 +60,7 @@ function AppLogoLink({
}) { }) {
const { onProjectClose } = useLspContext() const { onProjectClose } = useLspContext()
const wrapperClassName = const wrapperClassName =
"relative h-full grid place-content-center group p-1.5 before:block before:content-[''] before:absolute before:inset-0 before:bottom-2.5 before:z-[-1] before:bg-primary before:rounded-b-sm" "relative h-full grid place-content-center group p-1.5 before:block before:content-[''] before:absolute before:inset-0 before:bottom-1.5 before:z-[-1] before:bg-primary before:rounded-b-sm"
const logoClassName = 'w-auto h-4 text-chalkboard-10' const logoClassName = 'w-auto h-4 text-chalkboard-10'
return isDesktop() ? ( return isDesktop() ? (

View File

@ -218,6 +218,20 @@ export const Stream = () => {
} }
}, [IDLE, streamState]) }, [IDLE, streamState])
/**
* Play the vid
*/
useEffect(() => {
if (!kclManager.isExecuting) {
setTimeout(() => {
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
}
}, [kclManager.isExecuting])
useEffect(() => { useEffect(() => {
if ( if (
typeof window === 'undefined' || typeof window === 'undefined' ||
@ -229,15 +243,9 @@ export const Stream = () => {
// The browser complains if we try to load a new stream without pausing first. // The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream! // Do not immediately play the stream!
// we instead use a setTimeout to play the stream in the next event loop
try { try {
videoRef.current.srcObject = mediaStream videoRef.current.srcObject = mediaStream
videoRef.current.pause() videoRef.current.pause()
setTimeout(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
} catch (e) { } catch (e) {
console.warn('Attempted to pause stream while play was still loading', e) console.warn('Attempted to pause stream while play was still loading', e)
} }

View File

@ -150,31 +150,4 @@ describe('ToastUpdate tests', () => {
expect(restartButton).toBeEnabled() expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled() expect(dismissButton).toBeEnabled()
}) })
test('Happy path: external links render correctly', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- [Zoo](https://zoo.dev/)
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const zooDev = screen.getByText('Zoo', {
selector: 'a',
})
expect(zooDev).toHaveAttribute('href', 'https://zoo.dev/')
expect(zooDev).toHaveAttribute('target', '_blank')
expect(zooDev).toHaveAttribute('onClick')
})
}) })

View File

@ -1,9 +1,8 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { escape, Marked, MarkedOptions, unescape } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings' import { getReleaseUrl } from 'routes/Settings'
import { SafeRenderer } from 'lib/markdown'
export function ToastUpdate({ export function ToastUpdate({
version, version,
@ -20,14 +19,6 @@ export function ToastUpdate({
?.toLocaleLowerCase() ?.toLocaleLowerCase()
.includes('breaking') .includes('breaking')
const markedOptions: MarkedOptions = {
gfm: true,
breaks: true,
sanitize: true,
unescape,
escape,
}
return ( return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md"> <div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -67,8 +58,9 @@ export function ToastUpdate({
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto" className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, { __html: Marked.parse(releaseNotes, {
renderer: new SafeRenderer(markedOptions), gfm: true,
...markedOptions, breaks: true,
sanitize: true,
}), }),
}} }}
></div> ></div>

View File

@ -0,0 +1,42 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { UpdaterModal } from './UpdaterModal'
describe('UpdaterModal tests', () => {
test('Renders the modal', () => {
const callback = vi.fn()
const data = {
version: '1.2.3',
date: '2021-22-23T21:22:23Z',
body: 'This is the body.',
}
render(
<UpdaterModal
isOpen={true}
onReject={() => {}}
onResolve={callback}
instanceId=""
open={false}
close={(res) => {}}
version={data.version}
date={data.date}
body={data.body}
/>
)
expect(screen.getByTestId('update-version')).toHaveTextContent(data.version)
const updateButton = screen.getByTestId('update-button-update')
expect(updateButton).toBeEnabled()
fireEvent.click(updateButton)
expect(callback.mock.calls).toHaveLength(1)
expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: true })
const cancelButton = screen.getByTestId('update-button-cancel')
expect(cancelButton).toBeEnabled()
fireEvent.click(cancelButton)
expect(callback.mock.calls).toHaveLength(2)
expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: false })
})
})

View File

@ -0,0 +1,87 @@
import { create, InstanceProps } from 'react-modal-promise'
import { ActionButton } from './ActionButton'
import { Logo } from './Logo'
import { Marked } from '@ts-stack/markdown'
type ModalResolve = {
wantUpdate: boolean
}
type ModalReject = boolean
type UpdaterModalProps = InstanceProps<ModalResolve, ModalReject> & {
version: string
date?: string
body?: string
}
export const createUpdaterModal = create<
UpdaterModalProps,
ModalResolve,
ModalReject
>
export const UpdaterModal = ({
onResolve,
version,
date,
body,
}: UpdaterModalProps) => (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl min-w-[45rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="flex items-center">
<h1 className="flex-grow text-3xl font-bold">New version available!</h1>
<Logo className="h-9" />
</div>
<div className="my-4 flex items-baseline">
<span
className="px-3 py-1 text-xl rounded-full bg-energy-10 text-energy-80"
data-testid="update-version"
>
v{version}
</span>
<span className="ml-4 text-sm text-gray-400">Published on {date}</span>
</div>
{/* TODO: fix list bullets */}
{body && (
<div
className="my-4 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(body, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
)}
<div className="flex justify-between">
<ActionButton
Element="button"
onClick={() => onResolve({ wantUpdate: false })}
iconStart={{
icon: 'close',
bgClassName: 'bg-destroy-80',
iconClassName: 'text-destroy-20 group-hover:text-destroy-10',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
data-testid="update-button-cancel"
>
Not now
</ActionButton>
<ActionButton
Element="button"
onClick={() => onResolve({ wantUpdate: true })}
iconStart={{
icon: 'arrowRight',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50"
data-testid="update-button-update"
>
Update
</ActionButton>
</div>
</div>
</div>
)

View File

@ -40,7 +40,6 @@ export const setDiagnosticsEvent = setDiagnosticsAnnotation.of(true)
export default class EditorManager { export default class EditorManager {
private _copilotEnabled: boolean = true private _copilotEnabled: boolean = true
private _isAllTextSelected: boolean = false
private _isShiftDown: boolean = false private _isShiftDown: boolean = false
private _selectionRanges: Selections = { private _selectionRanges: Selections = {
otherSelections: [], otherSelections: [],
@ -118,10 +117,6 @@ export default class EditorManager {
}) })
} }
get isAllTextSelected() {
return this._isAllTextSelected
}
get editorView(): EditorView | null { get editorView(): EditorView | null {
return this._editorView return this._editorView
} }
@ -134,21 +129,6 @@ export default class EditorManager {
this._isShiftDown = isShiftDown this._isShiftDown = isShiftDown
} }
private selectionsWithSafeEnds(
selection: Array<Selection['codeRef']['range']>
): Array<[number, number]> {
if (!this._editorView) {
return selection.map((s): [number, number] => {
return [s[0], s[1]]
})
}
return selection.map((s): [number, number] => {
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
return [s[0], safeEnd]
})
}
set selectionRanges(selectionRanges: Selections) { set selectionRanges(selectionRanges: Selections) {
this._selectionRanges = selectionRanges this._selectionRanges = selectionRanges
} }
@ -174,9 +154,14 @@ export default class EditorManager {
} }
setHighlightRange(range: Array<Selection['codeRef']['range']>): void { setHighlightRange(range: Array<Selection['codeRef']['range']>): void {
const selectionsWithSafeEnds = this.selectionsWithSafeEnds(range) this._highlightRange = range.map((s): [number, number] => {
return [s[0], s[1]]
})
this._highlightRange = selectionsWithSafeEnds const selectionsWithSafeEnds = range.map((s): [number, number] => {
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
return [s[0], safeEnd]
})
if (this._editorView) { if (this._editorView) {
this._editorView.dispatch({ this._editorView.dispatch({
@ -317,20 +302,20 @@ export default class EditorManager {
} }
let codeBasedSelections = [] let codeBasedSelections = []
for (const selection of selections.graphSelections) { for (const selection of selections.graphSelections) {
const safeEnd = Math.min(
selection.codeRef.range[1],
this._editorView?.state.doc.length || selection.codeRef.range[1]
)
codeBasedSelections.push( codeBasedSelections.push(
EditorSelection.range(selection.codeRef.range[0], safeEnd) EditorSelection.range(
selection.codeRef.range[0],
selection.codeRef.range[1]
)
) )
} }
const end = codeBasedSelections.push(
selections.graphSelections[selections.graphSelections.length - 1].codeRef EditorSelection.cursor(
.range[1] selections.graphSelections[selections.graphSelections.length - 1]
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end) .codeRef.range[1]
codeBasedSelections.push(EditorSelection.cursor(safeEnd)) )
)
if (!this._editorView) { if (!this._editorView) {
return return
@ -367,16 +352,6 @@ export default class EditorManager {
return return
} }
this._isAllTextSelected = viewUpdate.state.selection.ranges.some(
(selection) => {
return (
// The user will need to select the empty new lines as well to be considered all of the text.
// CTRL+A is the best way to select all the text
selection.from === 0 && selection.to === viewUpdate.state.doc.length
)
}
)
const eventInfo = processCodeMirrorRanges({ const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges, codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges: this._selectionRanges, selectionRanges: this._selectionRanges,

View File

@ -10,11 +10,8 @@ import { AppStreamProvider } from 'AppState'
import { ToastUpdate } from 'components/ToastUpdate' import { ToastUpdate } from 'components/ToastUpdate'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { AUTO_UPDATER_TOAST_ID } from 'lib/constants' import { AUTO_UPDATER_TOAST_ID } from 'lib/constants'
import { initializeWindowExceptionHandler } from 'lib/exceptions'
markOnce('code/willAuth') markOnce('code/willAuth')
initializeWindowExceptionHandler()
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
// import { inspect } from '@xstate/inspect' // import { inspect } from '@xstate/inspect'

View File

@ -376,11 +376,7 @@ export class KclManager {
} }
this.ast = { ...ast } this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory // updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph( await this.engineCommandManager.updateArtifactGraph(this.ast)
this.ast,
execState.artifactCommands,
execState.artifacts
)
this._executeCallback() this._executeCallback()
if (!isInterrupted) { if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' }) sceneInfra.modelingSend({ type: 'code edit during sketch' })
@ -394,24 +390,6 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId) this._cancelTokens.delete(currentExecutionId)
markOnce('code/endExecuteAst') markOnce('code/endExecuteAst')
} }
/**
* This cleanup function is external and internal to the KclSingleton class.
* Since the WASM runtime can panic and the error cannot be caught in executeAst
* we need a global exception handler in exceptions.ts
* This file will interface with this cleanup as if it caught the original error
* to properly restore the TS application state.
*/
executeAstCleanUp() {
this.isExecuting = false
this.executeIsStale = null
this.engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,
})
markOnce('code/endExecuteAst')
}
// NOTE: this always updates the code state and editor. // NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever. // DO NOT CALL THIS from codemirror ever.
async executeAstMock( async executeAstMock(
@ -486,42 +464,13 @@ export class KclManager {
} }
async executeCode(zoomToFit?: boolean): Promise<void> { async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = await this.safeParse(codeManager.code) const ast = await this.safeParse(codeManager.code)
if (!ast) { if (!ast) {
this.clearAst() this.clearAst()
return return
} }
zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, zoomToFit)
this.ast = { ...ast } this.ast = { ...ast }
return this.executeAst({ zoomToFit }) return this.executeAst({ zoomToFit })
} }
/**
* This will override the zoom to fit to zoom into the model if the previous AST was empty.
* Workflows this improves,
* When someone comments the entire file then uncomments the entire file it zooms to the model
* When someone CRTL+A and deletes the code then adds the code back it zooms to the model
* When someone CRTL+A and copies new code into the editor it zooms to the model
*/
tryToZoomToFitOnCodeUpdate(
ast: Node<Program>,
zoomToFit: boolean | undefined
) {
const isAstEmpty = this._isAstEmpty(this._ast)
const isRequestedAstEmpty = this._isAstEmpty(ast)
// If the AST went from empty to not empty or
// If the user has all of the content selected and they copy new code in
if (
(isAstEmpty && !isRequestedAstEmpty) ||
editorManager.isAllTextSelected
) {
return true
}
return zoomToFit
}
async format() { async format() {
const originalCode = codeManager.code const originalCode = codeManager.code
const ast = await this.safeParse(originalCode) const ast = await this.safeParse(originalCode)

View File

@ -47,7 +47,7 @@ describe('parsing errors', () => {
const result = parse(code) const result = parse(code)
if (err(result)) throw result if (err(result)) throw result
const error = result.errors[0] const error = result.errors[0]
expect(error.message).toBe('Array is missing a closing bracket(`]`)') expect(error.message).toBe('Unexpected token: (')
expect(error.sourceRange).toEqual([28, 29, 0]) expect(error.sourceRange).toEqual([27, 28, 0])
}) })
}) })

View File

@ -10,7 +10,6 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Semantic error', msg: 'Semantic error',
sourceRange: [0, 1, true], sourceRange: [0, 1, true],
operations: [], operations: [],
artifactCommands: [],
}, },
{ {
name: '', name: '',
@ -19,7 +18,6 @@ describe('test kclErrToDiagnostic', () => {
msg: 'Type error', msg: 'Type error',
sourceRange: [4, 5, true], sourceRange: [4, 5, true],
operations: [], operations: [],
artifactCommands: [],
}, },
] ]
const diagnostics = kclErrorsToDiagnostics(errors) const diagnostics = kclErrorsToDiagnostics(errors)

View File

@ -5,7 +5,7 @@ import { posToOffset } from '@kittycad/codemirror-lsp-client'
import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol' import { Diagnostic as LspDiagnostic } from 'vscode-languageserver-protocol'
import { Text } from '@codemirror/state' import { Text } from '@codemirror/state'
import { EditorView } from 'codemirror' import { EditorView } from 'codemirror'
import { ArtifactCommand, SourceRange } from 'lang/wasm' import { SourceRange } from 'lang/wasm'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
type ExtractKind<T> = T extends { kind: infer K } ? K : never type ExtractKind<T> = T extends { kind: infer K } ? K : never
@ -14,141 +14,86 @@ export class KCLError extends Error {
sourceRange: SourceRange sourceRange: SourceRange
msg: string msg: string
operations: Operation[] operations: Operation[]
artifactCommands: ArtifactCommand[]
constructor( constructor(
kind: ExtractKind<RustKclError> | 'name', kind: ExtractKind<RustKclError> | 'name',
msg: string, msg: string,
sourceRange: SourceRange, sourceRange: SourceRange,
operations: Operation[], operations: Operation[]
artifactCommands: ArtifactCommand[]
) { ) {
super() super()
this.kind = kind this.kind = kind
this.msg = msg this.msg = msg
this.sourceRange = sourceRange this.sourceRange = sourceRange
this.operations = operations this.operations = operations
this.artifactCommands = artifactCommands
Object.setPrototypeOf(this, KCLError.prototype) Object.setPrototypeOf(this, KCLError.prototype)
} }
} }
export class KCLLexicalError extends KCLError { export class KCLLexicalError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('lexical', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('lexical', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLInternalError extends KCLError { export class KCLInternalError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('internal', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('internal', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSyntaxError extends KCLError { export class KCLSyntaxError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('syntax', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('syntax', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSyntaxError.prototype) Object.setPrototypeOf(this, KCLSyntaxError.prototype)
} }
} }
export class KCLSemanticError extends KCLError { export class KCLSemanticError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('semantic', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('semantic', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLSemanticError.prototype) Object.setPrototypeOf(this, KCLSemanticError.prototype)
} }
} }
export class KCLTypeError extends KCLError { export class KCLTypeError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('type', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('type', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLTypeError.prototype) Object.setPrototypeOf(this, KCLTypeError.prototype)
} }
} }
export class KCLUnimplementedError extends KCLError { export class KCLUnimplementedError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('unimplemented', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unimplemented', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnimplementedError.prototype) Object.setPrototypeOf(this, KCLUnimplementedError.prototype)
} }
} }
export class KCLUnexpectedError extends KCLError { export class KCLUnexpectedError extends KCLError {
constructor( constructor(msg: string, sourceRange: SourceRange, operations: Operation[]) {
msg: string, super('unexpected', msg, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super('unexpected', msg, sourceRange, operations, artifactCommands)
Object.setPrototypeOf(this, KCLUnexpectedError.prototype) Object.setPrototypeOf(this, KCLUnexpectedError.prototype)
} }
} }
export class KCLValueAlreadyDefined extends KCLError { export class KCLValueAlreadyDefined extends KCLError {
constructor( constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
key: string,
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super( super(
'name', 'name',
`Key ${key} was already defined elsewhere`, `Key ${key} was already defined elsewhere`,
sourceRange, sourceRange,
operations, operations
artifactCommands
) )
Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype) Object.setPrototypeOf(this, KCLValueAlreadyDefined.prototype)
} }
} }
export class KCLUndefinedValueError extends KCLError { export class KCLUndefinedValueError extends KCLError {
constructor( constructor(key: string, sourceRange: SourceRange, operations: Operation[]) {
key: string, super('name', `Key ${key} has not been defined`, sourceRange, operations)
sourceRange: SourceRange,
operations: Operation[],
artifactCommands: ArtifactCommand[]
) {
super(
'name',
`Key ${key} has not been defined`,
sourceRange,
operations,
artifactCommands
)
Object.setPrototypeOf(this, KCLUndefinedValueError.prototype) Object.setPrototypeOf(this, KCLUndefinedValueError.prototype)
} }
} }
@ -168,7 +113,6 @@ export function lspDiagnosticsToKclErrors(
'unexpected', 'unexpected',
message, message,
[posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true], [posToOffset(doc, range.start)!, posToOffset(doc, range.end)!, true],
[],
[] []
) )
) )

View File

@ -481,7 +481,6 @@ const theExtrude = startSketchOn('XY')
'undefined_value', 'undefined_value',
'memory item key `myVarZ` is not defined', 'memory item key `myVarZ` is not defined',
[129, 135, true], [129, 135, true],
[],
[] []
) )
) )

View File

@ -1,6 +1,6 @@
import { import {
Program, Program,
executor, _executor,
ProgramMemory, ProgramMemory,
kclLint, kclLint,
emptyExecState, emptyExecState,
@ -64,7 +64,7 @@ export async function executeAst({
try { try {
const execState = await (programMemoryOverride const execState = await (programMemoryOverride
? enginelessExecutor(ast, programMemoryOverride) ? enginelessExecutor(ast, programMemoryOverride)
: executor(ast, engineCommandManager)) : _executor(ast, engineCommandManager))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()

View File

@ -806,9 +806,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({ sketch002 = startSketchOn({
plane = { plane = {
origin = { x = 1, y = 2, z = 3 }, origin = { x = 1, y = 2, z = 3 },
xAxis = { x = 4, y = 5, z = 6 }, x_axis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 }, y_axis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 } z_axis = { x = 10, y = 11, z = 12 }
} }
}) })
|> startProfileAt([-12.55, 2.89], %) |> startProfileAt([-12.55, 2.89], %)
@ -862,9 +862,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({ sketch002 = startSketchOn({
plane = { plane = {
origin = { x = 1, y = 2, z = 3 }, origin = { x = 1, y = 2, z = 3 },
xAxis = { x = 4, y = 5, z = 6 }, x_axis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 }, y_axis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 } z_axis = { x = 10, y = 11, z = 12 }
} }
}) })
|> startProfileAt([-12.55, 2.89], %) |> startProfileAt([-12.55, 2.89], %)

View File

@ -374,37 +374,6 @@ export function loftSketches(
} }
} }
export function addSweep(
node: Node<Program>,
profileDeclarator: VariableDeclarator,
pathDeclarator: VariableDeclarator
): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
createIdentifier(profileDeclarator.id.name),
])
const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
export function revolveSketch( export function revolveSketch(
node: Node<Program>, node: Node<Program>,
pathToNode: PathToNode, pathToNode: PathToNode,
@ -1180,17 +1149,11 @@ export async function deleteFromSelection(
((selection?.artifact?.type === 'wall' || ((selection?.artifact?.type === 'wall' ||
selection?.artifact?.type === 'cap') && selection?.artifact?.type === 'cap') &&
varDec.node.init.type === 'PipeExpression') || varDec.node.init.type === 'PipeExpression') ||
selection.artifact?.type === 'sweep' || selection.artifact?.type === 'sweep'
selection.artifact?.type === 'plane' ||
!selection.artifact // aka expected to be a shell at this point
) { ) {
let extrudeNameToDelete = '' let extrudeNameToDelete = ''
let pathToNode: PathToNode | null = null let pathToNode: PathToNode | null = null
if ( if (selection.artifact?.type !== 'sweep') {
selection.artifact &&
selection.artifact.type !== 'sweep' &&
selection.artifact.type !== 'plane'
) {
const varDecName = varDec.node.id.name const varDecName = varDec.node.id.name
traverse(astClone, { traverse(astClone, {
enter: (node, path) => { enter: (node, path) => {
@ -1206,17 +1169,6 @@ export async function deleteFromSelection(
pathToNode = path pathToNode = path
extrudeNameToDelete = dec.id.name extrudeNameToDelete = dec.id.name
} }
if (
dec.init.type === 'CallExpression' &&
dec.init.callee.name === 'loft' &&
dec.init.arguments?.[0].type === 'ArrayExpression' &&
dec.init.arguments?.[0].elements.some(
(a) => a.type === 'Identifier' && a.name === varDecName
)
) {
pathToNode = path
extrudeNameToDelete = dec.id.name
}
} }
}, },
}) })
@ -1326,17 +1278,17 @@ export async function deleteFromSelection(
y: roundLiteral(faceDetails.origin.y), y: roundLiteral(faceDetails.origin.y),
z: roundLiteral(faceDetails.origin.z), z: roundLiteral(faceDetails.origin.z),
}), }),
xAxis: createObjectExpression({ x_axis: createObjectExpression({
x: roundLiteral(faceDetails.x_axis.x), x: roundLiteral(faceDetails.x_axis.x),
y: roundLiteral(faceDetails.x_axis.y), y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z), z: roundLiteral(faceDetails.x_axis.z),
}), }),
yAxis: createObjectExpression({ y_axis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x), x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y), y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z), z: roundLiteral(faceDetails.y_axis.z),
}), }),
zAxis: createObjectExpression({ z_axis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x), x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y), y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z), z: roundLiteral(faceDetails.z_axis.z),

View File

@ -61,18 +61,19 @@ export interface FilletParameters {
export type EdgeTreatmentParameters = ChamferParameters | FilletParameters export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
// Apply Edge Treatment (Fillet or Chamfer) To Selection // Apply Edge Treatment (Fillet or Chamfer) To Selection
export async function applyEdgeTreatmentToSelection( export function applyEdgeTreatmentToSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selections, selection: Selections,
parameters: EdgeTreatmentParameters parameters: EdgeTreatmentParameters
): Promise<void | Error> { ): void | Error {
// 1. clone and modify with edge treatment and tag // 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters) const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
if (err(result)) return result if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast // 2. update ast
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode) // eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
} }
export function modifyAstWithEdgeTreatmentAndTag( export function modifyAstWithEdgeTreatmentAndTag(
@ -290,7 +291,7 @@ export function getPathToExtrudeForSegmentSelection(
async function updateAstAndFocus( async function updateAstAndFocus(
modifiedAst: Node<Program>, modifiedAst: Node<Program>,
pathToEdgeTreatmentNode: Array<PathToNode> pathToEdgeTreatmentNode: Array<PathToNode>
): Promise<void> { ) {
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToEdgeTreatmentNode, focusPath: pathToEdgeTreatmentNode,
}) })

View File

@ -29,9 +29,7 @@ export function revolveSketch(
pathToSketchNode: PathToNode, pathToSketchNode: PathToNode,
shouldPipe = false, shouldPipe = false,
angle: Expr = createLiteral(4), angle: Expr = createLiteral(4),
axisOrEdge: string, axis: Selections
axis: string,
edge: Selections
): ):
| { | {
modifiedAst: Node<Program> modifiedAst: Node<Program>
@ -43,34 +41,31 @@ export function revolveSketch(
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode) const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode if (err(sketchNode)) return sketchNode
let generatedAxis // testing code
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
if (axisOrEdge === 'Edge') { const lineNode = getNodeFromPath<CallExpression>(
const pathToAxisSelection = getNodePathFromSourceRange( clonedAst,
clonedAst, pathToAxisSelection,
edge.graphSelections[0]?.codeRef.range 'CallExpression'
) )
const lineNode = getNodeFromPath<CallExpression>( if (err(lineNode)) return lineNode
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
const tagResult = mutateAstWithTagForSketchSegment( // TODO Kevin: What if |> close(%)?
clonedAst, // TODO Kevin: What if opposite edge
pathToAxisSelection // TODO Kevin: What if the edge isn't planar to the sketch?
) // TODO Kevin: add a tag.
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated // Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult if (err(tagResult)) return tagResult
const { tag } = tagResult const { tag } = tagResult
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
} else {
generatedAxis = createLiteral(axis)
}
/* Original Code */ /* Original Code */
const { node: sketchExpression } = sketchNode const { node: sketchExpression } = sketchNode
@ -96,12 +91,14 @@ export function revolveSketch(
shallowPath: sketchPathToDecleration, shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode } = sketchVariableDeclaratorNode
if (!generatedAxis) return new Error('Generated axis selection is missing.') const axisSelection = axis?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [ const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({ createObjectExpression({
angle: angle, angle: angle,
axis: generatedAxis, axis: getEdgeTagCall(tag, axisSelection),
}), }),
createIdentifier(sketchVariableDeclarator.id.name), createIdentifier(sketchVariableDeclarator.id.name),
]) ])

View File

@ -49,27 +49,17 @@ export function addShell({
return new Error("Couldn't find extrude") return new Error("Couldn't find extrude")
} }
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
// Get the sketch ref from the selection
// TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between. // TODO: this assumes the segment is piped directly from the sketch, with no intermediate `VariableDeclarator` between.
// We must find a technique for these situations that is robust to intermediate declarations // We must find a technique for these situations that is robust to intermediate declarations
const extrudeNode = getNodeFromPath<VariableDeclarator>( const sketchNode = getNodeFromPath<VariableDeclarator>(
modifiedAst, modifiedAst,
extrudeLookupResult.pathToExtrudeNode, graphSelection.codeRef.pathToNode,
'VariableDeclarator' 'VariableDeclarator'
) )
const segmentNode = getNodeFromPath<VariableDeclarator>( if (err(sketchNode)) {
modifiedAst, return sketchNode
extrudeLookupResult.pathToSegmentNode,
'VariableDeclarator'
)
if (err(extrudeNode) || err(segmentNode)) {
return new Error("Couldn't find extrude")
}
if (extrudeNode.node.init.type === 'CallExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToExtrudeNode
} else if (segmentNode.node.init.type === 'PipeExpression') {
pathToExtrudeNode = extrudeLookupResult.pathToSegmentNode
} else {
return new Error("Couldn't find extrude")
} }
const selectedArtifact = graphSelection.artifact const selectedArtifact = graphSelection.artifact

View File

@ -1,13 +1,7 @@
import { import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm'
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ArtifactCommand,
ExecState,
} from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { import {
OrderedCommand,
ResponseMap, ResponseMap,
createArtifactGraph, createArtifactGraph,
filterArtifacts, filterArtifacts,
@ -28,7 +22,6 @@ import * as d3 from 'd3-force'
import path from 'path' import path from 'path'
import pixelmatch from 'pixelmatch' import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs' import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/* /*
Note this is an integration test, these tests connect to our real dev server and make websocket commands. Note this is an integration test, these tests connect to our real dev server and make websocket commands.
@ -115,7 +108,7 @@ sketch002 = startSketchOn(offsetPlane001)
|> line([6.78, 15.01], %) |> line([6.78, 15.01], %)
` `
// add more code snippets here and use `getCommands` to get the artifactCommands and responseMap for more tests // add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
const codeToWriteCacheFor = { const codeToWriteCacheFor = {
exampleCode1, exampleCode1,
sketchOnFaceOnFaceEtc, sketchOnFaceOnFaceEtc,
@ -127,9 +120,8 @@ type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = { type CacheShape = {
[key in CodeKey]: { [key in CodeKey]: {
artifactCommands: ArtifactCommand[] orderedCommands: OrderedCommand[]
responseMap: ResponseMap responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
} }
} }
@ -159,9 +151,8 @@ beforeAll(async () => {
await kclManager.executeAst({ ast }) await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = { cacheToWriteToFileTemp[codeKey] = {
artifactCommands: kclManager.execState.artifactCommands, orderedCommands: engineCommandManager.orderedCommands,
responseMap: engineCommandManager.responseMap, responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
} }
} }
const cache = JSON.stringify(cacheToWriteToFileTemp) const cache = JSON.stringify(cacheToWriteToFileTemp)
@ -180,24 +171,18 @@ afterAll(() => {
describe('testing createArtifactGraph', () => { describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => { describe('code with offset planes and a sketch:', () => {
let ast: Node<Program> let ast: Program
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => { it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
artifactCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes') } = getCommands('exampleCodeOffsetPlanes')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it(`there should be one sketch`, () => { it(`there should be one sketch`, () => {
@ -232,23 +217,17 @@ describe('testing createArtifactGraph', () => {
}) })
}) })
describe('code with an extrusion, fillet and sketch of face:', () => { describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program> let ast: Program
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => { it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
artifactCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1') } = getCommands('exampleCode1')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it('there should be two planes for the extrusion and the sketch on face', () => { it('there should be two planes for the extrusion and the sketch on face', () => {
@ -333,23 +312,17 @@ describe('testing createArtifactGraph', () => {
}) })
describe(`code with sketches but no extrusions or other 3D elements`, () => { describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Node<Program> let ast: Program
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => { it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
artifactCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D') } = getCommands('exampleCodeNo3D')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
}) })
it('there should be two planes, one for each sketch path', () => { it('there should be two planes, one for each sketch path', () => {
@ -404,23 +377,17 @@ describe('testing createArtifactGraph', () => {
describe('capture graph of sketchOnFaceOnFace...', () => { describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => { describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program> let ast: Program
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => { it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished // putting this logic in here because describe blocks runs before beforeAll has finished
const { const {
artifactCommands, orderedCommands,
responseMap, responseMap,
ast: _ast, ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc') } = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast ast = _ast
theMap = createArtifactGraph({ theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
// Ostensibly this takes a screen shot of the graph of the artifactGraph // Ostensibly this takes a screen shot of the graph of the artifactGraph
// but it's it also tests that all of the id links are correct because if one // but it's it also tests that all of the id links are correct because if one
@ -432,21 +399,17 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
}) })
}) })
function getCommands( function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey) const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8') const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file) const parsed: CacheShape = JSON.parse(file)
// these either already exist from the last run, or were created in // these either already exist from the last run, or were created in
const artifactCommands = parsed[codeKey].artifactCommands const orderedCommands = parsed[codeKey].orderedCommands
const responseMap = parsed[codeKey].responseMap const responseMap = parsed[codeKey].responseMap
const execStateArtifacts = parsed[codeKey].execStateArtifacts
return { return {
artifactCommands, orderedCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts,
} }
} }
@ -672,30 +635,20 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => { describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => { it('should return an array of artifacts to update', () => {
const { artifactCommands, responseMap, ast, execStateArtifacts } = const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
getCommands('exampleCode1') const map = createArtifactGraph({ orderedCommands, responseMap, ast })
const map = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
const getArtifact = (id: string) => map.get(id) const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1' const currentPlaneId = 'UUID-1'
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => { const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
const artifactCommand = artifactCommands.find(
(a) => a.command.type === type
)
if (!artifactCommand) {
throw new Error(`No artifactCommand found for ${type}`)
}
const artifactsToUpdate = getArtifactsToUpdate({ const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand, orderedCommand: orderedCommands.find(
(a) =>
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
)!,
responseMap, responseMap,
getArtifact, getArtifact,
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}) })
return artifactsToUpdate.map(({ artifact }) => artifact) return artifactsToUpdate.map(({ artifact }) => artifact)
} }

View File

@ -1,15 +1,7 @@
import { import { PathToNode, Program, SourceRange } from 'lang/wasm'
ArtifactCommand,
ExecState,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string export type ArtifactId = string
@ -77,7 +69,7 @@ interface SegmentArtifactRich extends BaseArtifact {
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/ /** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact { interface SweepArtifact extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep' subType: 'extrusion' | 'revolve'
pathId: string pathId: string
surfaceIds: Array<string> surfaceIds: Array<string>
edgeIds: Array<string> edgeIds: Array<string>
@ -85,7 +77,7 @@ interface SweepArtifact extends BaseArtifact {
} }
interface SweepArtifactRich extends BaseArtifact { interface SweepArtifactRich extends BaseArtifact {
type: 'sweep' type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep' subType: 'extrusion' | 'revolve'
path: PathArtifact path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact> surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge> edges: Array<SweepEdge>
@ -151,47 +143,50 @@ type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
export interface ResponseMap { export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData [commandId: string]: OkWebSocketResponseData
} }
export interface OrderedCommand {
command: EngineCommand
range: SourceRange
}
/** Creates a graph of artifacts from a list of ordered commands and their responses /** Creates a graph of artifacts from a list of ordered commands and their responses
* muting the Map should happen entirely this function, other functions called within * muting the Map should happen entirely this function, other functions called within
* should return data on how to update the map, and not do so directly. * should return data on how to update the map, and not do so directly.
*/ */
export function createArtifactGraph({ export function createArtifactGraph({
artifactCommands, orderedCommands,
responseMap, responseMap,
ast, ast,
execStateArtifacts,
}: { }: {
artifactCommands: Array<ArtifactCommand> orderedCommands: Array<OrderedCommand>
responseMap: ResponseMap responseMap: ResponseMap
ast: Node<Program> ast: Program
execStateArtifacts: ExecState['artifacts']
}) { }) {
const myMap = new Map<ArtifactId, Artifact>() const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */ /** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = '' let currentPlaneId = ''
for (const artifactCommand of artifactCommands) { orderedCommands.forEach((orderedCommand) => {
if (artifactCommand.command.type === 'enable_sketch_mode') { if (orderedCommand.command?.type === 'modeling_cmd_req') {
currentPlaneId = artifactCommand.command.entity_id if (orderedCommand.command.cmd.type === 'enable_sketch_mode') {
} currentPlaneId = orderedCommand.command.cmd.entity_id
if (artifactCommand.command.type === 'sketch_mode_disable') { }
currentPlaneId = '' if (orderedCommand.command.cmd.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
} }
const artifactsToUpdate = getArtifactsToUpdate({ const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand, orderedCommand,
responseMap, responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id), getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}) })
artifactsToUpdate.forEach(({ id, artifact }) => { artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact) const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact) myMap.set(id, mergedArtifact)
}) })
} })
return myMap return myMap
} }
@ -232,30 +227,30 @@ function mergeArtifacts(
* can remove this. * can remove this.
*/ */
export function getArtifactsToUpdate({ export function getArtifactsToUpdate({
artifactCommand, orderedCommand: { command, range },
getArtifact, getArtifact,
responseMap, responseMap,
currentPlaneId, currentPlaneId,
ast, ast,
execStateArtifacts,
}: { }: {
artifactCommand: ArtifactCommand orderedCommand: OrderedCommand
responseMap: ResponseMap responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */ /** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId currentPlaneId: ArtifactId
ast: Node<Program> ast: Program
execStateArtifacts: ExecState['artifacts']
}): Array<{ }): Array<{
id: ArtifactId id: ArtifactId
artifact: Artifact artifact: Artifact
}> { }> {
const range = sourceRangeFromRust(artifactCommand.range)
const pathToNode = getNodePathFromSourceRange(ast, range) const pathToNode = getNodePathFromSourceRange(ast, range)
const id = artifactCommand.cmdId // expect all to be `modeling_cmd_req` as batch commands have
// already been expanded before being added to orderedCommands
if (command.type !== 'modeling_cmd_req') return []
const id = command.cmd_id
const response = responseMap[id] const response = responseMap[id]
const cmd = artifactCommand.command const cmd = command.cmd
const returnArr: ReturnType<typeof getArtifactsToUpdate> = [] const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) { if (cmd.type === 'make_plane' && range[1] !== 0) {
@ -377,11 +372,7 @@ export function getArtifactsToUpdate({
}) })
} }
return returnArr return returnArr
} else if ( } else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({ returnArr.push({
id, id,
@ -402,33 +393,6 @@ export function getArtifactsToUpdate({
artifact: { ...path, sweepId: id }, artifact: { ...path, sweepId: id },
}) })
return returnArr return returnArr
} else if (
cmd.type === 'loft' &&
response.type === 'modeling' &&
response.data.modeling_response.type === 'loft'
) {
returnArr.push({
id,
artifact: {
type: 'sweep',
subType: 'loft',
id,
// TODO: make sure to revisit this choice, don't think it matters for now
pathId: cmd.section_ids[0],
surfaceIds: [],
edgeIds: [],
codeRef: { range, pathToNode },
},
})
for (const sectionId of cmd.section_ids) {
const path = getArtifact(sectionId)
if (path?.type === 'path')
returnArr.push({
id: sectionId,
artifact: { ...path, sweepId: id },
})
}
return returnArr
} else if ( } else if (
cmd.type === 'solid3d_get_extrusion_face_info' && cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' && response?.type === 'modeling' &&

View File

@ -1,10 +1,10 @@
import { import {
ArtifactCommand,
defaultRustSourceRange, defaultRustSourceRange,
ExecState, defaultSourceRange,
Program, Program,
RustSourceRange, RustSourceRange,
SourceRange, SourceRange,
sourceRangeFromRust,
} from 'lang/wasm' } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
@ -20,6 +20,7 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { import {
ArtifactGraph, ArtifactGraph,
EngineCommand, EngineCommand,
OrderedCommand,
ResponseMap, ResponseMap,
createArtifactGraph, createArtifactGraph,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
@ -36,7 +37,6 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000 const pingIntervalMs = 5_000
@ -1303,7 +1303,7 @@ export enum EngineCommandManagerEvents {
* *
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response. * As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
* *
* Also all commands that are sent are kept track of in WASM artifactCommands and their responses are kept in {@link responseMap} * Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap}
* Both of these data structures are used to process the {@link artifactGraph}. * Both of these data structures are used to process the {@link artifactGraph}.
*/ */
@ -1329,7 +1329,12 @@ export class EngineCommandManager extends EventTarget {
[commandId: string]: PendingMessage [commandId: string]: PendingMessage
} = {} } = {}
/** /**
* A map of the responses to the WASM artifactCommands, when processing the commands into the artifactGraph, this response map allow * The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
* list of the individual commands, this is used to process all the commands into the artifactGraph
*/
orderedCommands: Array<OrderedCommand> = []
/**
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
* us to look up the response by command id * us to look up the response by command id
*/ */
responseMap: ResponseMap = {} responseMap: ResponseMap = {}
@ -1825,6 +1830,7 @@ export class EngineCommandManager extends EventTarget {
} }
} }
async startNewSession() { async startNewSession() {
this.orderedCommands = []
this.responseMap = {} this.responseMap = {}
await this.initPlanes() await this.initPlanes()
} }
@ -2067,6 +2073,28 @@ export class EngineCommandManager extends EventTarget {
isSceneCommand, isSceneCommand,
} }
if (message.command.type === 'modeling_cmd_req') {
this.orderedCommands.push({
command: message.command,
range: sourceRangeFromRust(message.range),
})
} else if (message.command.type === 'modeling_cmd_batch_req') {
message.command.requests.forEach((req) => {
const cmdId = req.cmd_id || ''
const range = cmdId
? sourceRangeFromRust(message.idToRangeMap[cmdId])
: defaultSourceRange()
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: req.cmd_id,
cmd: req.cmd,
}
this.orderedCommands.push({
command: cmd,
range,
})
})
}
this.engineConnection?.send(message.command) this.engineConnection?.send(message.command)
return promise return promise
} }
@ -2087,16 +2115,11 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise) Object.values(this.pendingCommands).map((a) => a.promise)
) )
} }
updateArtifactGraph( updateArtifactGraph(ast: Program) {
ast: Node<Program>,
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts']
) {
this.artifactGraph = createArtifactGraph({ this.artifactGraph = createArtifactGraph({
artifactCommands, orderedCommands: this.orderedCommands,
responseMap: this.responseMap, responseMap: this.responseMap,
ast, ast,
execStateArtifacts,
}) })
// TODO check if these still need to be deferred once e2e tests are working again. // TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) { if (this.artifactGraph.size) {

View File

@ -1,5 +1,4 @@
import { import init, {
init,
parse_wasm, parse_wasm,
recast_wasm, recast_wasm,
execute, execute,
@ -17,9 +16,7 @@ import {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
reloadModule, } from '../wasm-lib/pkg/wasm_lib'
} from 'lib/wasm_lib_wrapper'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -48,13 +45,7 @@ import { SourceRange as RustSourceRange } from 'wasm-lib/kcl/bindings/SourceRang
import { getAllCurrentSettings } from 'lib/settings/settingsUtils' import { getAllCurrentSettings } from 'lib/settings/settingsUtils'
import { Operation } from 'wasm-lib/kcl/bindings/Operation' import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs' import { KclErrorWithOutputs } from 'wasm-lib/kcl/bindings/KclErrorWithOutputs'
import { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
import { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/ArtifactCommand'
export type { ArtifactId } from 'wasm-lib/kcl/bindings/ArtifactId'
export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration' export type { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -153,7 +144,6 @@ export const wasmUrl = () => {
// Initialise the wasm module. // Initialise the wasm module.
const initialise = async () => { const initialise = async () => {
try { try {
await reloadModule()
const fullUrl = wasmUrl() const fullUrl = wasmUrl()
const input = await fetch(fullUrl) const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer() const buffer = await input.arrayBuffer()
@ -233,7 +223,6 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
} }
@ -258,8 +247,6 @@ export const isPathToNodeNumber = (
export interface ExecState { export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
operations: Operation[] operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
artifactCommands: ArtifactCommand[]
} }
/** /**
@ -270,8 +257,6 @@ export function emptyExecState(): ExecState {
return { return {
memory: ProgramMemory.empty(), memory: ProgramMemory.empty(),
operations: [], operations: [],
artifacts: {},
artifactCommands: [],
} }
} }
@ -279,8 +264,6 @@ function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
return { return {
memory: ProgramMemory.fromRaw(execOutcome.memory), memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations, operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
} }
} }
@ -523,6 +506,22 @@ export const executor = async (
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.startNewSession()
const _programMemory = await _executor(
node,
engineCommandManager,
programMemoryOverride
)
await engineCommandManager.waitForAllCommands()
return _programMemory
}
export const _executor = async (
node: Node<Program>,
engineCommandManager: EngineCommandManager,
programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => {
if (programMemoryOverride !== null && err(programMemoryOverride)) if (programMemoryOverride !== null && err(programMemoryOverride))
return Promise.reject(programMemoryOverride) return Promise.reject(programMemoryOverride)
@ -551,8 +550,7 @@ export const executor = async (
parsed.error.kind, parsed.error.kind,
parsed.error.msg, parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]), sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations, parsed.operations
parsed.artifactCommands
) )
return Promise.reject(kclError) return Promise.reject(kclError)
@ -612,7 +610,6 @@ export const modifyAstForSketch = async (
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
@ -682,7 +679,6 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.kind, parsed.kind,
parsed.msg, parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]), sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[] []
) )
} }

View File

@ -37,10 +37,6 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Sweep: {
path: Selections
profile: Selections
}
Loft: { Loft: {
selection: Selections selection: Selections
} }
@ -51,9 +47,7 @@ export type ModelingCommandSchema = {
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axisOrEdge: string axis: Selections
axis: string
edge: Selections
} }
Fillet: { Fillet: {
// todo // todo
@ -296,33 +290,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Sweep: {
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: true,
args: {
profile: {
inputType: 'selection',
selectionTypes: ['solid2D'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
},
path: {
inputType: 'selection',
selectionTypes: ['segment', 'path'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
},
},
},
Loft: { Loft: {
description: 'Create a 3D body by blending between two or more sketches', description: 'Create a 3D body by blending between two or more sketches',
icon: 'loft', icon: 'loft',
@ -357,10 +324,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
Revolve: { Revolve: {
description: 'Create a 3D body by rotating a sketch region about an axis.', description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve', icon: 'revolve',
status: 'development',
needsReview: true, needsReview: true,
args: { args: {
selection: { selection: {
@ -369,34 +336,9 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection multiple: false, // TODO: multiple selection
required: true, required: true,
skip: true, skip: true,
warningMessage:
'The revolve workflow is new and under tested. Please break it and report issues.',
},
axisOrEdge: {
inputType: 'options',
required: true,
defaultValue: 'Axis',
options: [
{ name: 'Axis', isCurrent: true, value: 'Axis' },
{ name: 'Edge', isCurrent: false, value: 'Edge' },
],
}, },
axis: { axis: {
required: (commandContext) => required: true,
['Axis'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'options',
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
],
},
edge: {
required: (commandContext) =>
['Edge'].includes(
commandContext.argumentsToSubmit.axisOrEdge as string
),
inputType: 'selection', inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'], selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false, multiple: false,

View File

@ -68,7 +68,7 @@ export const revolveAxisValidator = async ({
} }
const sketchSelection = artifact.pathId const sketchSelection = artifact.pathId
let edgeSelection = data.edge.graphSelections[0].artifact?.id let edgeSelection = data.axis.graphSelections[0].artifact?.id
if (!sketchSelection) { if (!sketchSelection) {
return 'Unable to revolve, sketch is missing' return 'Unable to revolve, sketch is missing'
@ -101,7 +101,7 @@ export const revolveAxisValidator = async ({
return true return true
} else { } else {
// return error message for the toast // return error message for the toast
return 'Unable to revolve with selected edge' return 'Unable to revolve with selected axis'
} }
} }

View File

@ -53,7 +53,6 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
LOFT: 'loft', LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell', SHELL: 'shell',
SEGMENT: 'seg', SEGMENT: 'seg',
REVOLVE: 'revolve', REVOLVE: 'revolve',

View File

@ -15,7 +15,6 @@ import {
StateMachineCommandSetSchema, StateMachineCommandSetSchema,
} from './commandTypes' } from './commandTypes'
import { DEV } from 'env' import { DEV } from 'env'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
interface CreateMachineCommandProps< interface CreateMachineCommandProps<
T extends AnyStateMachine, T extends AnyStateMachine,
@ -85,7 +84,7 @@ export function createMachineCommand<
} else if ('status' in commandConfig) { } else if ('status' in commandConfig) {
const { status } = commandConfig const { status } = commandConfig
if (status === 'inactive') return null if (status === 'inactive') return null
if (status === 'development' && !(DEV || IS_NIGHTLY_OR_DEBUG)) return null if (status === 'development' && !DEV) return null
} }
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined const icon = ('icon' in commandConfig && commandConfig.icon) || undefined

View File

@ -1,51 +0,0 @@
import { kclManager } from 'lib/singletons'
import { reloadModule, getModule } from 'lib/wasm_lib_wrapper'
import toast from 'react-hot-toast'
import { reportRejection } from './trap'
let initialized = false
/**
* WASM/Rust runtime can panic and the original try/catch/finally blocks will not trigger
* on the await promise. The interface will killed. This means we need to catch the error at
* the global/DOM level. This will have to interface with whatever controlflow that needs to be picked up
* within the error branch in the typescript to cover the application state.
*/
export const initializeWindowExceptionHandler = () => {
if (window && !initialized) {
window.addEventListener('error', (event) => {
void (async () => {
if (matchImportExportErrorCrash(event.message)) {
// do global singleton cleanup
kclManager.executeAstCleanUp()
toast.error(
'You have hit a KCL execution bug! Put your KCL code in a github issue to help us resolve this bug.'
)
try {
await reloadModule()
await getModule().default()
} catch (e) {
console.error('Failed to initialize wasm_lib')
console.error(e)
}
}
})().catch(reportRejection)
})
// Make sure we only initialize this event listener once
initialized = true
} else {
console.error(
`Failed to initialize, window: ${window}, initialized:${initialized}`
)
}
}
/**
* Specifically match a substring of the message error to detect an import export runtime issue
* when the WASM runtime panics
*/
const matchImportExportErrorCrash = (message: string): boolean => {
// called `Result::unwrap_throw()` on an `Err` value
const substringError = '`Result::unwrap_throw()` on an `Err` value'
return message.indexOf(substringError) !== -1 ? true : false
}

View File

@ -155,7 +155,7 @@ export interface components {
color?: string | null color?: string | null
/** @description The material that the filament is made of. */ /** @description The material that the filament is made of. */
material: components['schemas']['FilamentMaterial'] material: components['schemas']['FilamentMaterial']
/** @description The name of the filament, this is likely specific to the manufacturer. */ /** @description The name of the filament, this is likely specfic to the manufacturer. */
name?: string | null name?: string | null
} }
/** @description The material that the filament is made of. */ /** @description The material that the filament is made of. */

View File

@ -1,52 +0,0 @@
import { MarkedOptions, Renderer, unescape } from '@ts-stack/markdown'
import { openExternalBrowserIfDesktop } from './openWindow'
/**
* Main goal of this custom renderer is to prevent links from changing the current location
* this is specially important for the desktop app.
*/
export class SafeRenderer extends Renderer {
constructor(options: MarkedOptions) {
super(options)
// Attach a global function for non-react anchor elements that need safe navigation
window.openExternalLink = (e: React.MouseEvent<HTMLAnchorElement>) => {
openExternalBrowserIfDesktop()(e)
}
}
// Extended from https://github.com/ts-stack/markdown/blob/c5c1925c1153ca2fe9051c356ef0ddc60b3e1d6a/packages/markdown/src/renderer.ts#L116
link(href: string, title: string, text: string): string {
if (this.options.sanitize) {
let prot: string
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase()
} catch (e) {
return text
}
if (
// eslint-disable-next-line no-script-url
prot.indexOf('javascript:') === 0 ||
prot.indexOf('vbscript:') === 0 ||
prot.indexOf('data:') === 0
) {
return text
}
}
let out =
'<a onclick="openExternalLink(event)" target="_blank" href="' + href + '"'
if (title) {
out += ' title="' + title + '"'
}
out += '>' + text + '</a>'
return out
}
}

View File

@ -1,24 +1,6 @@
function takeScreenshotOfVideoStreamCanvas() { import html2canvas from 'html2canvas-pro'
const canvas = document.querySelector('[data-engine]')
const video = document.getElementById('video-stream')
if (
canvas &&
video &&
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
const videoCanvas = document.createElement('canvas')
videoCanvas.width = canvas.width
videoCanvas.height = canvas.height
const context = videoCanvas.getContext('2d')
context?.drawImage(video, 0, 0, videoCanvas.width, videoCanvas.height)
const url = videoCanvas.toDataURL('image/png')
return url
} else {
return ''
}
}
// Return a data URL (png format) of the screenshot of the current page.
export default async function screenshot(): Promise<string> { export default async function screenshot(): Promise<string> {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return Promise.reject( return Promise.reject(
@ -27,17 +9,11 @@ export default async function screenshot(): Promise<string> {
) )
) )
} }
return html2canvas(document.documentElement)
if (window.electron) { .then((canvas) => {
const canvas = document.querySelector('[data-engine]') return canvas.toDataURL()
if (canvas instanceof HTMLCanvasElement) { })
const url = await window.electron.takeElectronWindowScreenshot({ .catch((error) => {
width: canvas?.width || 500, return Promise.reject(error)
height: canvas?.height || 500, })
})
return url !== '' ? url : takeScreenshotOfVideoStreamCanvas()
}
}
return takeScreenshotOfVideoStreamCanvas()
} }

View File

@ -323,8 +323,7 @@ export function handleSelectionBatch({
resetAndSetEngineEntitySelectionCmds(selectionToEngine) resetAndSetEngineEntitySelectionCmds(selectionToEngine)
selections.graphSelections.forEach(({ codeRef }) => { selections.graphSelections.forEach(({ codeRef }) => {
if (codeRef.range?.[1]) { if (codeRef.range?.[1]) {
const safeEnd = Math.min(codeRef.range[1], codeManager.code.length) ranges.push(EditorSelection.cursor(codeRef.range[1]))
ranges.push(EditorSelection.cursor(safeEnd))
} }
}) })
if (ranges.length) if (ranges.length)

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