Compare commits

..

6 Commits

279 changed files with 2802 additions and 120760 deletions

View File

@ -1,3 +1,3 @@
[codespell]
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": [
"css-modules",
"jest",
"react",
"suggest-no-throw",
"@typescript-eslint"
],
"extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"semi": [
"error",
"never"

View File

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

View File

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

View File

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

View File

@ -337,47 +337,13 @@ For individual testing:
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
**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
# 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
just test
```
```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
KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1
```
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.
- **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)
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
* [`helix`](kcl/helix)
* [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole)
* [`hollow`](kcl/hollow)
* [`import`](kcl/import)
@ -82,7 +81,6 @@ layout: manual
* [`pi`](kcl/pi)
* [`polar`](kcl/polar)
* [`polygon`](kcl/polygon)
* [`pop`](kcl/pop)
* [`pow`](kcl/pow)
* [`profileStart`](kcl/profileStart)
* [`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"
excerpt: "A 2D axis and origin."
title: "AxisAndOrigin"
excerpt: "Axis and origin."
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"
excerpt: "A 2D axis or tagged edge."
title: "AxisOrEdgeReference"
excerpt: "Axis or tagged edge."
layout: manual
---
A 2D axis or tagged edge.
Axis or tagged edge.
**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"
excerpt: "Data for a helix."
excerpt: "Data for helices."
layout: manual
---
Data for a helix.
Data for helices.
**Type:** `object`
@ -19,8 +19,6 @@ Data for a helix.
| `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. | No |
| `radius` |`number`| Radius of the helix. | No |
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | 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 |
----
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.

View File

@ -16,6 +16,6 @@ Data for a mirror.
| 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 |
|----------|------|-------------|----------|
| `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 |

View File

@ -16,7 +16,7 @@ Data for a sweep.
| 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 |
| `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 {
public page: Page
public streamWrapper!: Locator
public loadingIndicator!: Locator
private exeIndicator!: Locator
constructor(page: Page) {
@ -65,8 +64,6 @@ export class SceneFixture {
this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.streamWrapper = page.getByTestId('stream')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
}
makeMouseHelpers = (

View File

@ -14,7 +14,6 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
@ -41,7 +40,6 @@ export class ToolbarFixture {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
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 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 = [
@ -862,173 +851,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
})
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 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(
'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' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
@ -199,7 +199,7 @@ 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' },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
@ -276,7 +276,7 @@ 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' },
async ({ context, page }, testInfo) => {
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()
})
})
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) {

View File

@ -39,8 +39,8 @@ test.describe('Sketch tests', () => {
${startProfileAt1}
|> arc({
radius = screwRadius,
angleStart = 0,
angleEnd = 360
angle_start = 0,
angle_end = 360
}, %)
part001 = startSketchOn('XY')
@ -60,8 +60,8 @@ test.describe('Sketch tests', () => {
|> yLine(wireOffset, %)
|> arc({
radius = wireRadius,
angleStart = 0,
angleEnd = 180
angle_start = 0,
angle_end = 180
}, %)
|> yLine(-wireOffset, %)
|> 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({
plane = {
origin = { x = 0, y = -50, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = -1, z = 0 }
x_axis = { x = 1, y = 0, z = 0 },
y_axis = { x = 0, y = 0, z = 1 },
z_axis = { x = 0, y = -1, z = 0 }
}
})`)
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
plane = {
origin = { x = 116.53, y = 0, z = 163.25 },
xAxis = { x = -0.81, y = 0, z = 0.58 },
yAxis = { x = 0, y = -1, z = 0 },
zAxis = { x = 0.58, y = 0, z = 0.81 }
x_axis = { x = -0.81, y = 0, z = 0.58 },
y_axis = { x = 0, y = -1, z = 0 },
z_axis = { x = 0.58, y = 0, z = 0.81 }
}
})`)
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
plane = {
origin = { x = -91.74, y = 0, z = 80.89 },
xAxis = { x = -0.66, y = 0, z = -0.75 },
yAxis = { x = 0, y = -1, z = 0 },
zAxis = { x = -0.75, y = 0, z = 0.66 }
x_axis = { x = -0.66, y = 0, z = -0.75 },
y_axis = { x = 0, y = -1, z = 0 },
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')
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()
// Click the Text-to-CAD command
await textToCadCommand.first().click()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
// Type the prompt.
@ -224,13 +224,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands')
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()
// Click the Text-to-CAD command
await textToCadCommand.first().click()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
@ -314,13 +314,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands')
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()
// Click the Text-to-CAD command
await textToCadCommand.first().click()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
const badPrompt = 'akjsndladflajbhflauweyf15;'
@ -392,13 +392,13 @@ test.describe('Text-to-CAD tests', () => {
const cmdSearchBar = page.getByPlaceholder('Search commands')
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()
// Click the Text-to-CAD command
await textToCadCommand.first().click()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
// Type the prompt.
@ -604,7 +604,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
await page.waitForTimeout(1000)
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
const prompt = page.getByText('Prompt')
await expect(prompt.first()).toBeVisible()
// Type the prompt.

18
flake.lock generated
View File

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

8
interface.d.ts vendored
View File

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

View File

@ -15,7 +15,7 @@
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.3",
"@codemirror/lint": "^6.8.4",
"@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
@ -26,7 +26,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "2.0.13",
"@kittycad/lib": "2.0.12",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",
@ -52,13 +52,13 @@
"react": "^18.3.1",
"react-dom": "^18.2.0",
"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-modal": "^3.16.3",
"react-modal": "^3.16.1",
"react-modal-promise": "^1.0.2",
"react-router-dom": "^6.28.0",
"sketch-helpers": "^0.0.4",
"three": "^0.172.0",
"three": "^0.166.1",
"ua-parser-js": "^1.0.37",
"uuid": "^11.0.2",
"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",
"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",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"lint": "eslint --max-warnings 0 --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 src e2e packages/codemirror-lsp-client",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -166,11 +166,13 @@
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.1",
"@types/react-modal": "^3.16.3",
"@types/three": "^0.172.0",
"@types/three": "^0.163.0",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17",
@ -180,12 +182,11 @@
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.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",
"happy-dom": "^16.3.0",
"happy-dom": "^15.11.7",
"http-server": "^14.1.1",
"husky": "^9.1.5",
"kill-port": "^2.0.1",
@ -199,7 +200,6 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -17,7 +17,7 @@
statement[@isGroup=Statement] {
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 } |
ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression }
@ -85,7 +85,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
Number { "." @digit+ | @digit+ ("." @digit+)? }
Number { "." @digit+ | @digit+ ("." @digit*)? }
@precedence { Number, "." }
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
const length = parseInt(match[1])
if (Number.isNaN(length))
if (isNaN(length))
return Promise.reject(new Error('invalid content length'))
// slice the headers since we now have the content length

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ function AppLogoLink({
}) {
const { onProjectClose } = useLspContext()
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'
return isDesktop() ? (

View File

@ -218,6 +218,20 @@ export const Stream = () => {
}
}, [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(() => {
if (
typeof window === 'undefined' ||
@ -229,15 +243,9 @@ export const Stream = () => {
// The browser complains if we try to load a new stream without pausing first.
// Do not immediately play the stream!
// we instead use a setTimeout to play the stream in the next event loop
try {
videoRef.current.srcObject = mediaStream
videoRef.current.pause()
setTimeout(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
})
} catch (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(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 { ActionButton } from './ActionButton'
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 { SafeRenderer } from 'lib/markdown'
export function ToastUpdate({
version,
@ -20,14 +19,6 @@ export function ToastUpdate({
?.toLocaleLowerCase()
.includes('breaking')
const markedOptions: MarkedOptions = {
gfm: true,
breaks: true,
sanitize: true,
unescape,
escape,
}
return (
<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">
@ -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"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
renderer: new SafeRenderer(markedOptions),
...markedOptions,
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></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 {
private _copilotEnabled: boolean = true
private _isAllTextSelected: boolean = false
private _isShiftDown: boolean = false
private _selectionRanges: Selections = {
otherSelections: [],
@ -118,10 +117,6 @@ export default class EditorManager {
})
}
get isAllTextSelected() {
return this._isAllTextSelected
}
get editorView(): EditorView | null {
return this._editorView
}
@ -134,21 +129,6 @@ export default class EditorManager {
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) {
this._selectionRanges = selectionRanges
}
@ -174,9 +154,14 @@ export default class EditorManager {
}
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) {
this._editorView.dispatch({
@ -317,20 +302,20 @@ export default class EditorManager {
}
let codeBasedSelections = []
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(
EditorSelection.range(selection.codeRef.range[0], safeEnd)
EditorSelection.range(
selection.codeRef.range[0],
selection.codeRef.range[1]
)
)
}
const end =
selections.graphSelections[selections.graphSelections.length - 1].codeRef
.range[1]
const safeEnd = Math.min(end, this._editorView?.state.doc.length || end)
codeBasedSelections.push(EditorSelection.cursor(safeEnd))
codeBasedSelections.push(
EditorSelection.cursor(
selections.graphSelections[selections.graphSelections.length - 1]
.codeRef.range[1]
)
)
if (!this._editorView) {
return
@ -367,16 +352,6 @@ export default class EditorManager {
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({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges: this._selectionRanges,

View File

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

View File

@ -376,11 +376,7 @@ export class KclManager {
}
this.ast = { ...ast }
// updateArtifactGraph relies on updated executeState/programMemory
await this.engineCommandManager.updateArtifactGraph(
this.ast,
execState.artifactCommands,
execState.artifacts
)
await this.engineCommandManager.updateArtifactGraph(this.ast)
this._executeCallback()
if (!isInterrupted) {
sceneInfra.modelingSend({ type: 'code edit during sketch' })
@ -394,24 +390,6 @@ export class KclManager {
this._cancelTokens.delete(currentExecutionId)
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.
// DO NOT CALL THIS from codemirror ever.
async executeAstMock(
@ -486,42 +464,13 @@ export class KclManager {
}
async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = await this.safeParse(codeManager.code)
if (!ast) {
this.clearAst()
return
}
zoomToFit = this.tryToZoomToFitOnCodeUpdate(ast, zoomToFit)
this.ast = { ...ast }
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() {
const originalCode = codeManager.code
const ast = await this.safeParse(originalCode)

View File

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

View File

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

View File

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

View File

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

View File

@ -806,9 +806,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({
plane = {
origin = { x = 1, y = 2, z = 3 },
xAxis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 }
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
}
})
|> startProfileAt([-12.55, 2.89], %)
@ -862,9 +862,9 @@ sketch001 = startSketchOn('XZ')
sketch002 = startSketchOn({
plane = {
origin = { x = 1, y = 2, z = 3 },
xAxis = { x = 4, y = 5, z = 6 },
yAxis = { x = 7, y = 8, z = 9 },
zAxis = { x = 10, y = 11, z = 12 }
x_axis = { x = 4, y = 5, z = 6 },
y_axis = { x = 7, y = 8, z = 9 },
z_axis = { x = 10, y = 11, z = 12 }
}
})
|> 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(
node: Node<Program>,
pathToNode: PathToNode,
@ -1180,17 +1149,11 @@ export async function deleteFromSelection(
((selection?.artifact?.type === 'wall' ||
selection?.artifact?.type === 'cap') &&
varDec.node.init.type === 'PipeExpression') ||
selection.artifact?.type === 'sweep' ||
selection.artifact?.type === 'plane' ||
!selection.artifact // aka expected to be a shell at this point
selection.artifact?.type === 'sweep'
) {
let extrudeNameToDelete = ''
let pathToNode: PathToNode | null = null
if (
selection.artifact &&
selection.artifact.type !== 'sweep' &&
selection.artifact.type !== 'plane'
) {
if (selection.artifact?.type !== 'sweep') {
const varDecName = varDec.node.id.name
traverse(astClone, {
enter: (node, path) => {
@ -1206,17 +1169,6 @@ export async function deleteFromSelection(
pathToNode = path
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),
z: roundLiteral(faceDetails.origin.z),
}),
xAxis: createObjectExpression({
x_axis: createObjectExpression({
x: roundLiteral(faceDetails.x_axis.x),
y: roundLiteral(faceDetails.x_axis.y),
z: roundLiteral(faceDetails.x_axis.z),
}),
yAxis: createObjectExpression({
y_axis: createObjectExpression({
x: roundLiteral(faceDetails.y_axis.x),
y: roundLiteral(faceDetails.y_axis.y),
z: roundLiteral(faceDetails.y_axis.z),
}),
zAxis: createObjectExpression({
z_axis: createObjectExpression({
x: roundLiteral(faceDetails.z_axis.x),
y: roundLiteral(faceDetails.z_axis.y),
z: roundLiteral(faceDetails.z_axis.z),

View File

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

View File

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

View File

@ -49,27 +49,17 @@ export function addShell({
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.
// We must find a technique for these situations that is robust to intermediate declarations
const extrudeNode = getNodeFromPath<VariableDeclarator>(
const sketchNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
extrudeLookupResult.pathToExtrudeNode,
graphSelection.codeRef.pathToNode,
'VariableDeclarator'
)
const segmentNode = getNodeFromPath<VariableDeclarator>(
modifiedAst,
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")
if (err(sketchNode)) {
return sketchNode
}
const selectedArtifact = graphSelection.artifact

View File

@ -1,13 +1,7 @@
import {
makeDefaultPlanes,
assertParse,
initPromise,
Program,
ArtifactCommand,
ExecState,
} from 'lang/wasm'
import { makeDefaultPlanes, assertParse, initPromise, Program } from 'lang/wasm'
import { Models } from '@kittycad/lib'
import {
OrderedCommand,
ResponseMap,
createArtifactGraph,
filterArtifacts,
@ -28,7 +22,6 @@ import * as d3 from 'd3-force'
import path from 'path'
import pixelmatch from 'pixelmatch'
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.
@ -115,7 +108,7 @@ sketch002 = startSketchOn(offsetPlane001)
|> 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 = {
exampleCode1,
sketchOnFaceOnFaceEtc,
@ -127,9 +120,8 @@ type CodeKey = keyof typeof codeToWriteCacheFor
type CacheShape = {
[key in CodeKey]: {
artifactCommands: ArtifactCommand[]
orderedCommands: OrderedCommand[]
responseMap: ResponseMap
execStateArtifacts: ExecState['artifacts']
}
}
@ -159,9 +151,8 @@ beforeAll(async () => {
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
artifactCommands: kclManager.execState.artifactCommands,
orderedCommands: engineCommandManager.orderedCommands,
responseMap: engineCommandManager.responseMap,
execStateArtifacts: kclManager.execState.artifacts,
}
}
const cache = JSON.stringify(cacheToWriteToFileTemp)
@ -180,24 +171,18 @@ afterAll(() => {
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Node<Program>
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeOffsetPlanes')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
})
it(`there should be one sketch`, () => {
@ -232,23 +217,17 @@ describe('testing createArtifactGraph', () => {
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCode1')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
})
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`, () => {
let ast: Node<Program>
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('exampleCodeNo3D')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
})
it('there should be two planes, one for each sketch path', () => {
@ -404,23 +377,17 @@ describe('testing createArtifactGraph', () => {
describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Node<Program>
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
const {
artifactCommands,
orderedCommands,
responseMap,
ast: _ast,
execStateArtifacts,
} = getCommands('sketchOnFaceOnFaceEtc')
ast = _ast
theMap = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
// 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
@ -432,21 +399,17 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
})
})
function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)
// 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 execStateArtifacts = parsed[codeKey].execStateArtifacts
return {
artifactCommands,
orderedCommands,
responseMap,
ast,
execStateArtifacts,
}
}
@ -672,30 +635,20 @@ async function GraphTheGraph(
describe('testing getArtifactsToUpdate', () => {
it('should return an array of artifacts to update', () => {
const { artifactCommands, responseMap, ast, execStateArtifacts } =
getCommands('exampleCode1')
const map = createArtifactGraph({
artifactCommands,
responseMap,
ast,
execStateArtifacts,
})
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
const map = createArtifactGraph({ orderedCommands, responseMap, ast })
const getArtifact = (id: string) => map.get(id)
const currentPlaneId = 'UUID-1'
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({
artifactCommand,
orderedCommand: orderedCommands.find(
(a) =>
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
)!,
responseMap,
getArtifact,
currentPlaneId,
ast,
execStateArtifacts,
})
return artifactsToUpdate.map(({ artifact }) => artifact)
}

View File

@ -1,15 +1,7 @@
import {
ArtifactCommand,
ExecState,
PathToNode,
Program,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { PathToNode, Program, SourceRange } from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
import { Node } from 'wasm-lib/kcl/bindings/Node'
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*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
subType: 'extrusion' | 'revolve'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
@ -85,7 +77,7 @@ interface SweepArtifact extends BaseArtifact {
}
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
subType: 'extrusion' | 'revolve'
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge>
@ -151,47 +143,50 @@ type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
export interface ResponseMap {
[commandId: string]: OkWebSocketResponseData
}
export interface OrderedCommand {
command: EngineCommand
range: SourceRange
}
/** 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
* should return data on how to update the map, and not do so directly.
*/
export function createArtifactGraph({
artifactCommands,
orderedCommands,
responseMap,
ast,
execStateArtifacts,
}: {
artifactCommands: Array<ArtifactCommand>
orderedCommands: Array<OrderedCommand>
responseMap: ResponseMap
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
ast: Program
}) {
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
for (const artifactCommand of artifactCommands) {
if (artifactCommand.command.type === 'enable_sketch_mode') {
currentPlaneId = artifactCommand.command.entity_id
}
if (artifactCommand.command.type === 'sketch_mode_disable') {
currentPlaneId = ''
orderedCommands.forEach((orderedCommand) => {
if (orderedCommand.command?.type === 'modeling_cmd_req') {
if (orderedCommand.command.cmd.type === 'enable_sketch_mode') {
currentPlaneId = orderedCommand.command.cmd.entity_id
}
if (orderedCommand.command.cmd.type === 'sketch_mode_disable') {
currentPlaneId = ''
}
}
const artifactsToUpdate = getArtifactsToUpdate({
artifactCommand,
orderedCommand,
responseMap,
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
execStateArtifacts,
})
artifactsToUpdate.forEach(({ id, artifact }) => {
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
myMap.set(id, mergedArtifact)
})
}
})
return myMap
}
@ -232,30 +227,30 @@ function mergeArtifacts(
* can remove this.
*/
export function getArtifactsToUpdate({
artifactCommand,
orderedCommand: { command, range },
getArtifact,
responseMap,
currentPlaneId,
ast,
execStateArtifacts,
}: {
artifactCommand: ArtifactCommand
orderedCommand: OrderedCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Node<Program>
execStateArtifacts: ExecState['artifacts']
ast: Program
}): Array<{
id: ArtifactId
artifact: Artifact
}> {
const range = sourceRangeFromRust(artifactCommand.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 cmd = artifactCommand.command
const cmd = command.cmd
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr
if (cmd.type === 'make_plane' && range[1] !== 0) {
@ -377,11 +372,7 @@ export function getArtifactsToUpdate({
})
}
return returnArr
} else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
} else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({
id,
@ -402,33 +393,6 @@ export function getArtifactsToUpdate({
artifact: { ...path, sweepId: id },
})
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 (
cmd.type === 'solid3d_get_extrusion_face_info' &&
response?.type === 'modeling' &&

View File

@ -1,10 +1,10 @@
import {
ArtifactCommand,
defaultRustSourceRange,
ExecState,
defaultSourceRange,
Program,
RustSourceRange,
SourceRange,
sourceRangeFromRust,
} from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib'
@ -20,6 +20,7 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import {
ArtifactGraph,
EngineCommand,
OrderedCommand,
ResponseMap,
createArtifactGraph,
} from 'lang/std/artifactGraph'
@ -36,7 +37,6 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable.
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.
*
* 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}.
*/
@ -1329,7 +1329,12 @@ export class EngineCommandManager extends EventTarget {
[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
*/
responseMap: ResponseMap = {}
@ -1825,6 +1830,7 @@ export class EngineCommandManager extends EventTarget {
}
}
async startNewSession() {
this.orderedCommands = []
this.responseMap = {}
await this.initPlanes()
}
@ -2067,6 +2073,28 @@ export class EngineCommandManager extends EventTarget {
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)
return promise
}
@ -2087,16 +2115,11 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(
ast: Node<Program>,
artifactCommands: ArtifactCommand[],
execStateArtifacts: ExecState['artifacts']
) {
updateArtifactGraph(ast: Program) {
this.artifactGraph = createArtifactGraph({
artifactCommands,
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,
ast,
execStateArtifacts,
})
// TODO check if these still need to be deferred once e2e tests are working again.
if (this.artifactGraph.size) {

View File

@ -1,5 +1,4 @@
import {
init,
import init, {
parse_wasm,
recast_wasm,
execute,
@ -17,9 +16,7 @@ import {
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
reloadModule,
} from 'lib/wasm_lib_wrapper'
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
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 { Operation } from 'wasm-lib/kcl/bindings/Operation'
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 { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
@ -153,7 +144,6 @@ export const wasmUrl = () => {
// Initialise the wasm module.
const initialise = async () => {
try {
await reloadModule()
const fullUrl = wasmUrl()
const input = await fetch(fullUrl)
const buffer = await input.arrayBuffer()
@ -233,7 +223,6 @@ export const parse = (code: string | Error): ParseResult | Error => {
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
}
@ -258,8 +247,6 @@ export const isPathToNodeNumber = (
export interface ExecState {
memory: ProgramMemory
operations: Operation[]
artifacts: { [key in ArtifactId]?: Artifact }
artifactCommands: ArtifactCommand[]
}
/**
@ -270,8 +257,6 @@ export function emptyExecState(): ExecState {
return {
memory: ProgramMemory.empty(),
operations: [],
artifacts: {},
artifactCommands: [],
}
}
@ -279,8 +264,6 @@ function execStateFromRust(execOutcome: RustExecOutcome): ExecState {
return {
memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations,
artifacts: execOutcome.artifacts,
artifactCommands: execOutcome.artifactCommands,
}
}
@ -523,6 +506,22 @@ export const executor = async (
return Promise.reject(programMemoryOverride)
// 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))
return Promise.reject(programMemoryOverride)
@ -551,8 +550,7 @@ export const executor = async (
parsed.error.kind,
parsed.error.msg,
sourceRangeFromRust(parsed.error.sourceRanges[0]),
parsed.operations,
parsed.artifactCommands
parsed.operations
)
return Promise.reject(kclError)
@ -612,7 +610,6 @@ export const modifyAstForSketch = async (
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
@ -682,7 +679,6 @@ export function programMemoryInit(): ProgramMemory | Error {
parsed.kind,
parsed.msg,
sourceRangeFromRust(parsed.sourceRanges[0]),
[],
[]
)
}

View File

@ -37,10 +37,6 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
Sweep: {
path: Selections
profile: Selections
}
Loft: {
selection: Selections
}
@ -51,9 +47,7 @@ export type ModelingCommandSchema = {
Revolve: {
selection: Selections
angle: KclCommandValue
axisOrEdge: string
axis: string
edge: Selections
axis: Selections
}
Fillet: {
// 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: {
description: 'Create a 3D body by blending between two or more sketches',
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: {
description: 'Create a 3D body by rotating a sketch region about an axis.',
icon: 'revolve',
status: 'development',
needsReview: true,
args: {
selection: {
@ -369,34 +336,9 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
multiple: false, // TODO: multiple selection
required: 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: {
required: (commandContext) =>
['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
),
required: true,
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,

View File

@ -68,7 +68,7 @@ export const revolveAxisValidator = async ({
}
const sketchSelection = artifact.pathId
let edgeSelection = data.edge.graphSelections[0].artifact?.id
let edgeSelection = data.axis.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
@ -101,7 +101,7 @@ export const revolveAxisValidator = async ({
return true
} else {
// 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',
EXTRUDE: 'extrude',
LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell',
SEGMENT: 'seg',
REVOLVE: 'revolve',

View File

@ -15,7 +15,6 @@ import {
StateMachineCommandSetSchema,
} from './commandTypes'
import { DEV } from 'env'
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
interface CreateMachineCommandProps<
T extends AnyStateMachine,
@ -85,7 +84,7 @@ export function createMachineCommand<
} else if ('status' in commandConfig) {
const { status } = commandConfig
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

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
/** @description The material that the filament is made of. */
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
}
/** @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() {
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 ''
}
}
import html2canvas from 'html2canvas-pro'
// Return a data URL (png format) of the screenshot of the current page.
export default async function screenshot(): Promise<string> {
if (typeof window === 'undefined') {
return Promise.reject(
@ -27,17 +9,11 @@ export default async function screenshot(): Promise<string> {
)
)
}
if (window.electron) {
const canvas = document.querySelector('[data-engine]')
if (canvas instanceof HTMLCanvasElement) {
const url = await window.electron.takeElectronWindowScreenshot({
width: canvas?.width || 500,
height: canvas?.height || 500,
})
return url !== '' ? url : takeScreenshotOfVideoStreamCanvas()
}
}
return takeScreenshotOfVideoStreamCanvas()
return html2canvas(document.documentElement)
.then((canvas) => {
return canvas.toDataURL()
})
.catch((error) => {
return Promise.reject(error)
})
}

View File

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

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