diff --git a/README.md b/README.md index b46165faa..e1b722094 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ yarn install followed by: ``` -yarn build:wasm-dev +yarn build:wasm ``` or if you have the gh cli installed @@ -66,15 +66,15 @@ or if you have the gh cli installed ./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle ``` -That will build the WASM binary and put in the `public` dir (though gitignored) +That will build the WASM binary and put in the `public` dir (though gitignored). -finally, to run the web app only, run: +Finally, to run the web app only, run: ``` yarn start ``` -If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens ofcourse, then navigate to localhost:3000 again. Note that navigating to localhost:3000/signin removes your token so you will need to set the token again. +If you're not an KittyCAD employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to localhost:3000 again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again. ### Development environment variables @@ -91,13 +91,13 @@ Third-Party Cookies". ## Desktop -To spin up the desktop app, `yarn install` and `yarn build:wasm-dev` need to have been done before hand then +To spin up the desktop app, `yarn install` and `yarn build:wasm` need to have been done before hand then ``` -yarn electron:start +yarn tron:start ``` -This will start the application and hot-reload on changed. +This will start the application and hot-reload on changes. Devtools can be opened with the usual Cmd/Ctrl-Shift-I. @@ -128,7 +128,18 @@ Before you submit a contribution PR to this repo, please ensure that: ## Release a new version -#### 1. Bump the versions by running `./make-release.sh` and create a Cut Release PR +#### 1. Bump the versions by running `./make-release.sh` + +The `./make-release.sh` script has git commands to pull main but to be sure you can run the following git commands to have a fresh `main` locally. + +``` +git branch -D main +git checkout main +git pull origin +./make-release.sh +# Copy within the back ticks and paste the stdout of the change log +git push --set-upstream origin +``` That will create the branch with the updated json files for you: - run `./make-release.sh` or `./make-release.sh patch` for a patch update; @@ -137,28 +148,32 @@ That will create the branch with the updated json files for you: After it runs you should just need the push the branch and open a PR. -**Important:** It needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate. +#### 2. Create a Cut Release PR + +When you open the PR copy the change log from the output of the `./make-release.sh` script into the description of the PR. + +**Important:** Pull request title needs to be prefixed with `Cut release v` to build in release mode and a few other things to test in the best context possible, the intent would be for instance to have `Cut release v1.2.3` for the `v1.2.3` release candidate. The PR may then serve as a place to discuss the human-readable changelog and extra QA. The `make-release.sh` tool suggests a changelog for you too to be used as PR description, just make sure to delete lines that are not user facing. -#### 2. Smoke test artifacts from the Cut Release PR +#### 3. Manually test artifacts from the Cut Release PR The release builds can be find under the `artifact` zip, at the very bottom of the `ci` action page for each commit on this branch. -We don't have a strict process, but click around and check for anything obvious, posting results as comments in the Cut Release PR. +Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the Cut Release PR. The other `ci` output in Cut Release PRs is `updater-test`, because we don't have a way to test this fully automated, we have a semi-automated process. Download updater-test zip file, install the app, run it, expect an updater prompt to a dummy v0.99.99, install it and check that the app comes back at that version (on both macOS and Windows). -#### 3. Merge the Cut Release PR +#### 4. Merge the Cut Release PR This will kick the `create-release` action, that creates a _Draft_ release out of this Cut Release PR merge after less than a minute, with the new version as title and Cut Release PR as description. -#### 4. Publish the release +#### 5. Publish the release Head over to https://github.com/KittyCAD/modeling-app/releases, the draft release corresponding to the merged Cut Release PR should show up at the top as _Draft_. Click on it, verify the content, and hit _Publish_. -#### 5. Profit +#### 6. Profit A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. @@ -319,7 +334,16 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin ```bash cd src/wasm-lib -cargo test +KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1 +``` + +Where `XXX` is an API token from the production engine (NOT the dev environment). + +We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using + +``` +cd src/wasm-lib +KITTYCAD_API_TOKEN=XXX cargo run nextest ``` ### Mapping CI CD jobs to local commands diff --git a/docs/kcl/reduce.md b/docs/kcl/reduce.md index bf47ce401..70de7bfdf 100644 --- a/docs/kcl/reduce.md +++ b/docs/kcl/reduce.md @@ -18,12 +18,12 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue | Name | Type | Description | Required | |----------|------|-------------|----------| | `array` | [`[KclValue]`](/docs/kcl/types/KclValue) | | Yes | -| `start` | [`KclValue`](/docs/kcl/types/KclValue) | A memory item. | Yes | +| `start` | [`KclValue`](/docs/kcl/types/KclValue) | Any KCL value. | Yes | | `reduce_fn` | `FunctionParam` | | Yes | ### Returns -[`KclValue`](/docs/kcl/types/KclValue) - A memory item. +[`KclValue`](/docs/kcl/types/KclValue) - Any KCL value. ### Examples diff --git a/docs/kcl/std.json b/docs/kcl/std.json index ad7c572bd..e271bd2ee 100644 --- a/docs/kcl/std.json +++ b/docs/kcl/std.json @@ -80912,7 +80912,7 @@ }, "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -84468,7 +84468,7 @@ "type": "null", "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -88028,7 +88028,7 @@ }, "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -112440,7 +112440,7 @@ }, "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -115993,7 +115993,7 @@ "schema": { "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "title": "KclValue", - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -116389,7 +116389,7 @@ ], "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -119945,7 +119945,7 @@ "type": "null", "definitions": { "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -123499,7 +123499,7 @@ "schema": { "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "title": "KclValue", - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", @@ -127037,7 +127037,7 @@ } }, "KclValue": { - "description": "A memory item.", + "description": "Any KCL value.", "oneOf": [ { "type": "object", diff --git a/docs/kcl/types/KclValue.md b/docs/kcl/types/KclValue.md index 6c0bf166a..280a85263 100644 --- a/docs/kcl/types/KclValue.md +++ b/docs/kcl/types/KclValue.md @@ -1,10 +1,10 @@ --- title: "KclValue" -excerpt: "A memory item." +excerpt: "Any KCL value." layout: manual --- -A memory item. +Any KCL value. @@ -80,7 +80,7 @@ A plane. |----------|------|-------------|----------| | `type` |enum: `Plane`| | No | | `id` |`string`| The id of the plane. | No | -| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No | +| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | | `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | @@ -183,8 +183,8 @@ Data for an imported geometry. | Property | Type | Description | Required | |----------|------|-------------|----------| | `type` |enum: `Function`| | No | -| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No | -| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No | +| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No | +| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | diff --git a/e2e/playwright/debug-pane.spec.ts b/e2e/playwright/debug-pane.spec.ts new file mode 100644 index 000000000..c59a354f9 --- /dev/null +++ b/e2e/playwright/debug-pane.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test' + +import { getUtils, setup, tearDown } from './test-utils' + +test.beforeEach(async ({ context, page }, testInfo) => { + await setup(context, page, testInfo) +}) + +test.afterEach(async ({ page }, testInfo) => { + await tearDown(page, testInfo) +}) + +function countNewlines(input: string): number { + let count = 0 + for (const char of input) { + if (char === '\n') { + count++ + } + } + return count +} + +test.describe('Debug pane', () => { + test('Artifact IDs in the artifact graph are stable across code edits', async ({ + page, + context, + }) => { + const code = `sketch001 = startSketchOn('XZ') + |> startProfileAt([0, 0], %) +|> line([1, 1], %) +` + const u = await getUtils(page) + await page.setViewportSize({ width: 1200, height: 500 }) + + const tree = page.getByTestId('debug-feature-tree') + const segment = tree.locator('li', { + hasText: 'segIds:', + hasNotText: 'paths:', + }) + + await test.step('Test setup', async () => { + await u.waitForAuthSkipAppStart() + await u.openKclCodePanel() + await u.openDebugPanel() + // Set the code in the code editor. + await u.codeLocator.click() + await page.keyboard.type(code, { delay: 0 }) + // Scroll to the feature tree. + await tree.scrollIntoViewIfNeeded() + // Expand the feature tree. + await tree.getByText('Feature Tree').click() + // Just expanded the details, making the element taller, so scroll again. + await tree.getByText('Plane').first().scrollIntoViewIfNeeded() + }) + // Extract the artifact IDs from the debug feature tree. + const initialSegmentIds = await segment.innerText({ timeout: 5_000 }) + // The artifact ID should include a UUID. + expect(initialSegmentIds).toMatch( + /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/ + ) + await test.step('Move cursor to the bottom of the code editor', async () => { + // Focus on the code editor. + await u.codeLocator.click() + // Make sure the cursor is at the end of the code. + const lines = countNewlines(code) + 1 + for (let i = 0; i < lines; i++) { + await page.keyboard.press('ArrowDown') + } + }) + await test.step('Enter a comment', async () => { + await page.keyboard.type('|> line([2, 2], %)', { delay: 0 }) + // Wait for keyboard input debounce and updated artifact graph. + await page.waitForTimeout(1000) + }) + const newSegmentIds = await segment.innerText() + // Strip off the closing bracket. + const initialIds = initialSegmentIds.slice(0, initialSegmentIds.length - 1) + expect(newSegmentIds.slice(0, initialIds.length)).toEqual(initialIds) + }) +}) diff --git a/e2e/playwright/projects.spec.ts b/e2e/playwright/projects.spec.ts index 623632189..153ce8313 100644 --- a/e2e/playwright/projects.spec.ts +++ b/e2e/playwright/projects.spec.ts @@ -856,23 +856,30 @@ test( 'Deleting projects, can delete individual project, can still create projects after deleting all', { tag: '@electron' }, async ({ browserName }, testInfo) => { + const projectData = [ + ['router-template-slate', 'cylinder.kcl'], + ['bracket', 'focusrite_scarlett_mounting_braket.kcl'], + ['lego', 'lego.kcl'], + ] + const { electronApp, page } = await setupElectron({ testInfo, + folderSetupFn: async (dir) => { + // Do these serially to ensure the order is correct + for (const [name, file] of projectData) { + await fsp.mkdir(join(dir, name), { recursive: true }) + await fsp.copyFile( + executorInputPath(file), + join(dir, name, `main.kcl`) + ) + // Wait 1s between each project to ensure the order is correct + await new Promise((r) => setTimeout(r, 1_000)) + } + }, }) await page.setViewportSize({ width: 1200, height: 500 }) - page.on('console', console.log) - // we need to create the folders so that the order is correct - // creating them ahead of time with fs tools means they all have the same timestamp - await createProject({ - name: 'router-template-slate', - page, - returnHome: true, - }) - await createProject({ name: 'bracket', page, returnHome: true }) - await createProject({ name: 'lego', page, returnHome: true }) - await test.step('delete the middle project, i.e. the bracket project', async () => { const project = page.getByTestId('project-link').getByText('bracket') @@ -972,8 +979,26 @@ test( 'Can sort projects on home page', { tag: '@electron' }, async ({ browserName }, testInfo) => { + const projectData = [ + ['router-template-slate', 'cylinder.kcl'], + ['bracket', 'focusrite_scarlett_mounting_braket.kcl'], + ['lego', 'lego.kcl'], + ] + const { electronApp, page } = await setupElectron({ testInfo, + folderSetupFn: async (dir) => { + // Do these serially to ensure the order is correct + for (const [name, file] of projectData) { + await fsp.mkdir(join(dir, name), { recursive: true }) + await fsp.copyFile( + executorInputPath(file), + join(dir, name, `main.kcl`) + ) + // Wait 1s between each project to ensure the order is correct + await new Promise((r) => setTimeout(r, 1_000)) + } + }, }) await page.setViewportSize({ width: 1200, height: 500 }) @@ -981,16 +1006,6 @@ test( page.on('console', console.log) - // we need to create the folders so that the order is correct - // creating them ahead of time with fs tools means they all have the same timestamp - await createProject({ - name: 'router-template-slate', - page, - returnHome: true, - }) - await createProject({ name: 'bracket', page, returnHome: true }) - await createProject({ name: 'lego', page, returnHome: true }) - await test.step('should be shorted by modified initially', async () => { const lastModifiedButton = page.getByRole('button', { name: 'Last Modified', @@ -1086,7 +1101,9 @@ test( await expect(page.getByText('No Projects found')).toBeVisible() await createProject({ name: 'project-000', page, returnHome: true }) - await expect(page.getByText('project-000')).toBeVisible() + await expect( + page.getByTestId('project-link').getByText('project-000') + ).toBeVisible() await page.getByTestId('project-link').getByText('project-000').click() diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 2ea7832ce..438158062 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -521,7 +521,6 @@ test( const startXPx = 600 // Equip the rectangle tool - await page.getByRole('button', { name: 'line Line', exact: true }).click() await page .getByRole('button', { name: 'rectangle Corner rectangle', exact: true }) .click() diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index f94bd2630..ad78a77e5 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index 127b0f318..5847ff9bc 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png index e391ba2d0..8de7a180a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/testing-selections.spec.ts b/e2e/playwright/testing-selections.spec.ts index eb3aba829..c162f910f 100644 --- a/e2e/playwright/testing-selections.spec.ts +++ b/e2e/playwright/testing-selections.spec.ts @@ -1208,6 +1208,12 @@ extrude001 = extrude(50, sketch001) test('Deselecting line tool should mean nothing happens on click', async ({ page, }) => { + /** + * If the line tool is clicked when the state is 'No Points' it will exit Sketch mode. + * This is the same exact workflow as pressing ESC. + * + * To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool. + */ const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) @@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001) 200 ) + // Clicks the XZ Plane in the page await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).toHaveText( @@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001) await page.waitForTimeout(600) + // Place a point because the line tool will exit if no points are pressed + await page.mouse.click(650, 200) + await page.waitForTimeout(600) + + // Code before exiting the tool let previousCodeContent = await page.locator('.cm-content').innerText() // deselect the line tool by clicking it diff --git a/interface.d.ts b/interface.d.ts index 7319c69c1..66938f10d 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -69,9 +69,13 @@ export interface IElectronAPI { kittycad: (access: string, args: any) => any listMachines: () => Promise getMachineApiIp: () => Promise + onUpdateDownloadStart: ( + callback: (value: { version: string }) => void + ) => Electron.IpcRenderer onUpdateDownloaded: ( callback: (value: string) => void ) => Electron.IpcRenderer + onUpdateError: (callback: (value: { error: Error }) => void) => Electron appRestart: () => void } diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index e6d68e8e0..7a545cce8 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -408,6 +408,7 @@ export async function deleteSegment({ const testExecute = await executeAst({ ast: modifiedAst, + idGenerator: kclManager.execState.idGenerator, useFakeExecutor: true, engineCommandManager: engineCommandManager, }) diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index ad5cc9144..5faea3e78 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -391,12 +391,14 @@ export class SceneEntities { const { truncatedAst, programMemoryOverride, variableDeclarationName } = prepared - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast: truncatedAst, useFakeExecutor: true, engineCommandManager: this.engineCommandManager, programMemoryOverride, + idGenerator: kclManager.execState.idGenerator, }) + const programMemory = execState.memory const sketch = sketchFromPathToNode({ pathToNode: sketchPathToNode, ast: maybeModdedAst, @@ -801,12 +803,14 @@ export class SceneEntities { updateRectangleSketch(sketchInit, x, y, tags[0]) } - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast: truncatedAst, useFakeExecutor: true, engineCommandManager: this.engineCommandManager, programMemoryOverride, + idGenerator: kclManager.execState.idGenerator, }) + const programMemory = execState.memory this.sceneProgramMemory = programMemory const sketch = sketchFromKclValue( programMemory.get(variableDeclarationName), @@ -848,12 +852,14 @@ export class SceneEntities { await kclManager.executeAstMock(_ast) sceneInfra.modelingSend({ type: 'Finish rectangle' }) - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast: _ast, useFakeExecutor: true, engineCommandManager: this.engineCommandManager, programMemoryOverride, + idGenerator: kclManager.execState.idGenerator, }) + const programMemory = execState.memory // Prepare to update the THREEjs scene this.sceneProgramMemory = programMemory @@ -965,12 +971,14 @@ export class SceneEntities { modded = moddedResult.modifiedAst } - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast: modded, useFakeExecutor: true, engineCommandManager: this.engineCommandManager, programMemoryOverride, + idGenerator: kclManager.execState.idGenerator, }) + const programMemory = execState.memory this.sceneProgramMemory = programMemory const sketch = sketchFromKclValue( programMemory.get(variableDeclarationName), @@ -1317,12 +1325,14 @@ export class SceneEntities { // don't want to mod the user's code yet as they have't committed to the change yet // plus this would be the truncated ast being recast, it would be wrong codeManager.updateCodeEditor(code) - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast: truncatedAst, useFakeExecutor: true, engineCommandManager: this.engineCommandManager, programMemoryOverride, + idGenerator: kclManager.execState.idGenerator, }) + const programMemory = execState.memory this.sceneProgramMemory = programMemory const maybeSketch = programMemory.get(variableDeclarationName) diff --git a/src/components/AvailableVarsHelpers.tsx b/src/components/AvailableVarsHelpers.tsx index a3020544b..56355a5f0 100644 --- a/src/components/AvailableVarsHelpers.tsx +++ b/src/components/AvailableVarsHelpers.tsx @@ -157,7 +157,7 @@ export function useCalc({ engineCommandManager, useFakeExecutor: true, programMemoryOverride: kclManager.programMemory.clone(), - }).then(({ programMemory }) => { + }).then(({ execState }) => { const resultDeclaration = ast.body.find( (a) => a.type === 'VariableDeclaration' && @@ -166,7 +166,7 @@ export function useCalc({ const init = resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.declarations?.[0]?.init - const result = programMemory?.get('__result__')?.value + const result = execState.memory?.get('__result__')?.value setCalcResult(typeof result === 'number' ? String(result) : 'NAN') init && setValueNode(init) }) diff --git a/src/components/DebugDisplayObj.tsx b/src/components/DebugDisplayObj.tsx new file mode 100644 index 000000000..55b6ec244 --- /dev/null +++ b/src/components/DebugDisplayObj.tsx @@ -0,0 +1,111 @@ +import { isArray, isNonNullable } from 'lib/utils' +import { useRef, useState } from 'react' + +type Primitive = string | number | bigint | boolean | symbol | null | undefined + +export type GenericObj = { + type?: string + [key: string]: GenericObj | Primitive | Array +} + +/** + * Display an array of objects or primitives for debug purposes. Nullable values + * are displayed so that relative indexes are preserved. + */ +export function DebugDisplayArray({ + arr, + filterKeys, +}: { + arr: Array + filterKeys: string[] +}) { + return ( + <> + {arr.map((obj, index) => { + return ( +
+ {obj && typeof obj === 'object' ? ( + + ) : isNonNullable(obj) ? ( + {obj.toString()} + ) : ( + {obj} + )} +
+ ) + })} + + ) +} + +/** + * Display an object as a tree for debug purposes. Nullable values are omitted. + * The only other property treated specially is the type property, which is + * assumed to be a string. + */ +export function DebugDisplayObj({ + obj, + filterKeys, +}: { + obj: GenericObj + filterKeys: string[] +}) { + const ref = useRef(null) + const hasCursor = false + const [isCollapsed, setIsCollapsed] = useState(false) + return ( +
+      {isCollapsed ? (
+        
+      ) : (
+        
+          
+          
    + {Object.entries(obj).map(([key, value]) => { + if (filterKeys.includes(key)) { + return null + } else if (isArray(value)) { + return ( +
  • + {`${key}: [`} + + {']'} +
  • + ) + } else if (typeof value === 'object' && value !== null) { + return ( +
  • + {key}: + +
  • + ) + } else if (isNonNullable(value)) { + return ( +
  • + {key}: {value.toString()} +
  • + ) + } + return null + })} +
+
+ )} +
+ ) +} diff --git a/src/components/DebugFeatureTree.tsx b/src/components/DebugFeatureTree.tsx new file mode 100644 index 000000000..df9e53bc1 --- /dev/null +++ b/src/components/DebugFeatureTree.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react' +import { engineCommandManager } from 'lib/singletons' +import { + ArtifactGraph, + expandPlane, + PlaneArtifactRich, +} from 'lang/std/artifactGraph' +import { DebugDisplayArray, GenericObj } from './DebugDisplayObj' + +export function DebugFeatureTree() { + const featureTree = useMemo(() => { + return computeTree(engineCommandManager.artifactGraph) + }, [engineCommandManager.artifactGraph]) + + const filterKeys: string[] = ['__meta', 'codeRef', 'pathToNode'] + return ( +
+ Feature Tree + {featureTree.length > 0 ? ( +
+          
+        
+ ) : ( +

(Empty)

+ )} +
+ ) +} + +function computeTree(artifactGraph: ArtifactGraph): GenericObj[] { + let items: GenericObj[] = [] + + const planes: PlaneArtifactRich[] = [] + for (const artifact of artifactGraph.values()) { + if (artifact.type === 'plane') { + planes.push(expandPlane(artifact, artifactGraph)) + } + } + const extraRichPlanes: GenericObj[] = planes.map((plane) => { + return plane as any as GenericObj + }) + items = items.concat(extraRichPlanes) + + return items +} diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 0b0d73dbf..dfe76e2c5 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -149,6 +149,13 @@ export const ModelingMachineProvider = ({ }, 'sketch exit execute': ({ context: { store } }) => { ;(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' }, + }) + sceneInfra.camControls.syncDirection = 'clientToEngine' if (cameraProjection.current === 'perspective') { diff --git a/src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx b/src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx index 40f6169d4..f96cb9ef6 100644 --- a/src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx @@ -1,3 +1,4 @@ +import { DebugFeatureTree } from 'components/DebugFeatureTree' import { AstExplorer } from '../../AstExplorer' import { EngineCommands } from '../../EngineCommands' import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp' @@ -12,6 +13,7 @@ export const DebugPane = () => { + ) diff --git a/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx b/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx index 65ac1db1a..6b1931875 100644 --- a/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx +++ b/src/components/ModelingSidebar/ModelingPanes/MemoryPane.test.tsx @@ -29,8 +29,8 @@ describe('processMemory', () => { |> lineTo([2.15, 4.32], %) // |> rx(90, %)` const ast = parse(code) - const programMemory = await enginelessExecutor(ast, ProgramMemory.empty()) - const output = processMemory(programMemory) + const execState = await enginelessExecutor(ast, ProgramMemory.empty()) + const output = processMemory(execState.memory) expect(output.myVar).toEqual(5) expect(output.otherVar).toEqual(3) expect(output).toEqual({ diff --git a/src/index.tsx b/src/index.tsx index 6e9b4f4a7..4ce2fff5a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,6 +8,7 @@ import ModalContainer from 'react-modal-promise' import { isDesktop } from 'lib/isDesktop' import { AppStreamProvider } from 'AppState' import { ToastUpdate } from 'components/ToastUpdate' +import { AUTO_UPDATER_TOAST_ID } from 'lib/constants' // uncomment for xstate inspector // import { DEV } from 'env' @@ -53,7 +54,22 @@ root.render( // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals() -isDesktop() && +if (isDesktop()) { + // Listen for update download progress to begin + // to show a loading toast. + window.electron.onUpdateDownloadStart(() => { + const message = `Downloading app update...` + console.log(message) + toast.loading(message, { id: AUTO_UPDATER_TOAST_ID }) + }) + // Listen for update download errors to show + // an error toast and clear the loading toast. + window.electron.onUpdateError(({ error }) => { + console.error(error) + toast.error('An error occurred while downloading the update.', { + id: AUTO_UPDATER_TOAST_ID, + }) + }) window.electron.onUpdateDownloaded((version: string) => { const message = `A new update (${version}) was downloaded and will be available next time you open the app.` console.log(message) @@ -64,6 +80,7 @@ isDesktop() && window.electron.appRestart() }, }), - { duration: 30000 } + { duration: 30000, id: AUTO_UPDATER_TOAST_ID } ) }) +} diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index d29d953bf..4126847ef 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -8,6 +8,8 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants' import { CallExpression, + emptyExecState, + ExecState, initPromise, parse, PathToNode, @@ -42,6 +44,7 @@ export class KclManager { }, digest: null, } + private _execState: ExecState = emptyExecState() private _programMemory: ProgramMemory = ProgramMemory.empty() lastSuccessfulProgramMemory: ProgramMemory = ProgramMemory.empty() private _logs: string[] = [] @@ -72,11 +75,21 @@ export class KclManager { get programMemory() { return this._programMemory } - set programMemory(programMemory) { + // This is private because callers should be setting the entire execState. + private set programMemory(programMemory) { this._programMemory = programMemory this._programMemoryCallBack(programMemory) } + set execState(execState) { + this._execState = execState + this.programMemory = execState.memory + } + + get execState() { + return this._execState + } + get logs() { return this._logs } @@ -253,8 +266,9 @@ export class KclManager { // Make sure we clear before starting again. End session will do this. this.engineCommandManager?.endSession() await this.ensureWasmInit() - const { logs, errors, programMemory, isInterrupted } = await executeAst({ + const { logs, errors, execState, isInterrupted } = await executeAst({ ast, + idGenerator: this.execState.idGenerator, engineCommandManager: this.engineCommandManager, }) @@ -264,7 +278,7 @@ export class KclManager { this.lints = await lintAst({ ast: ast }) sceneInfra.modelingSend({ type: 'code edit during sketch' }) - defaultSelectionFilter(programMemory, this.engineCommandManager) + defaultSelectionFilter(execState.memory, this.engineCommandManager) if (args.zoomToFit) { let zoomObjectId: string | undefined = '' @@ -295,12 +309,20 @@ export class KclManager { this._cancelTokens.delete(currentExecutionId) return } + + // Exit sketch mode if the AST is empty + if (this._isAstEmpty(ast)) { + await this.disableSketchMode() + } + this.logs = logs // Do not add the errors since the program was interrupted and the error is not a real KCL error this.addKclErrors(isInterrupted ? [] : errors) - this.programMemory = programMemory + // Reset the next ID index so that we reuse the previous IDs next time. + execState.idGenerator.nextId = 0 + this.execState = execState if (!errors.length) { - this.lastSuccessfulProgramMemory = programMemory + this.lastSuccessfulProgramMemory = execState.memory } this.ast = { ...ast } this._executeCallback() @@ -338,17 +360,19 @@ export class KclManager { await codeManager.writeToFile() this._ast = { ...newAst } - const { logs, errors, programMemory } = await executeAst({ + const { logs, errors, execState } = await executeAst({ ast: newAst, + idGenerator: this.execState.idGenerator, engineCommandManager: this.engineCommandManager, useFakeExecutor: true, }) this._logs = logs this._kclErrors = errors - this._programMemory = programMemory + this._execState = execState + this._programMemory = execState.memory if (!errors.length) { - this.lastSuccessfulProgramMemory = programMemory + this.lastSuccessfulProgramMemory = execState.memory } if (updates !== 'artifactRanges') return @@ -553,6 +577,24 @@ export class KclManager { defaultSelectionFilter() { defaultSelectionFilter(this.programMemory, this.engineCommandManager) } + + /** + * We can send a single command of 'enable_sketch_mode' or send this in a batched request. + * When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished + * code could leave the state of the application in sketch mode on the engine side. + */ + async disableSketchMode() { + await this.engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { type: 'sketch_mode_disable' }, + }) + } + + // Determines if there is no KCL code which means it is executing a blank KCL file + _isAstEmpty(ast: Program) { + return ast.start === 0 && ast.end === 0 && ast.body.length === 0 + } } function defaultSelectionFilter( diff --git a/src/lang/artifact.test.ts b/src/lang/artifact.test.ts index bb20262b6..099a752b2 100644 --- a/src/lang/artifact.test.ts +++ b/src/lang/artifact.test.ts @@ -14,9 +14,9 @@ const mySketch001 = startSketchOn('XY') |> lineTo([-1.59, -1.54], %) |> lineTo([0.46, -5.82], %) // |> rx(45, %)` - const programMemory = await enginelessExecutor(parse(code)) + const execState = await enginelessExecutor(parse(code)) // @ts-ignore - const sketch001 = programMemory?.get('mySketch001') + const sketch001 = execState.memory.get('mySketch001') expect(sketch001).toEqual({ type: 'UserVal', __meta: [{ sourceRange: [46, 71] }], @@ -68,9 +68,9 @@ const mySketch001 = startSketchOn('XY') |> lineTo([0.46, -5.82], %) // |> rx(45, %) |> extrude(2, %)` - const programMemory = await enginelessExecutor(parse(code)) + const execState = await enginelessExecutor(parse(code)) // @ts-ignore - const sketch001 = programMemory?.get('mySketch001') + const sketch001 = execState.memory.get('mySketch001') expect(sketch001).toEqual({ type: 'Solid', id: expect.any(String), @@ -148,9 +148,10 @@ const sk2 = startSketchOn('XY') |> extrude(2, %) ` - const programMemory = await enginelessExecutor(parse(code)) + const execState = await enginelessExecutor(parse(code)) + const programMemory = execState.memory // @ts-ignore - const geos = [programMemory?.get('theExtrude'), programMemory?.get('sk2')] + const geos = [programMemory.get('theExtrude'), programMemory.get('sk2')] expect(geos).toEqual([ { type: 'Solid', diff --git a/src/lang/executor.test.ts b/src/lang/executor.test.ts index cb29934ea..0049d15be 100644 --- a/src/lang/executor.test.ts +++ b/src/lang/executor.test.ts @@ -443,6 +443,6 @@ async function exe( ) { const ast = parse(code) - const result = await enginelessExecutor(ast, programMemory) - return result + const execState = await enginelessExecutor(ast, programMemory) + return execState.memory } diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index d436a6120..138b75809 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -4,11 +4,14 @@ import { ProgramMemory, programMemoryInit, kclLint, + emptyExecState, + ExecState, } from 'lang/wasm' import { enginelessExecutor } from 'lib/testHelpers' import { EngineCommandManager } from 'lang/std/engineConnection' import { KCLError } from 'lang/errors' import { Diagnostic } from '@codemirror/lint' +import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' export type ToolTip = | 'lineTo' @@ -47,16 +50,18 @@ export async function executeAst({ engineCommandManager, useFakeExecutor = false, programMemoryOverride, + idGenerator, }: { ast: Program engineCommandManager: EngineCommandManager useFakeExecutor?: boolean programMemoryOverride?: ProgramMemory + idGenerator?: IdGenerator isInterrupted?: boolean }): Promise<{ logs: string[] errors: KCLError[] - programMemory: ProgramMemory + execState: ExecState isInterrupted: boolean }> { try { @@ -65,15 +70,21 @@ export async function executeAst({ // eslint-disable-next-line @typescript-eslint/no-floating-promises engineCommandManager.startNewSession() } - const programMemory = await (useFakeExecutor + const execState = await (useFakeExecutor ? enginelessExecutor(ast, programMemoryOverride || programMemoryInit()) - : _executor(ast, programMemoryInit(), engineCommandManager, false)) + : _executor( + ast, + programMemoryInit(), + idGenerator, + engineCommandManager, + false + )) await engineCommandManager.waitForAllCommands() return { logs: [], errors: [], - programMemory, + execState, isInterrupted: false, } } catch (e: any) { @@ -89,7 +100,7 @@ export async function executeAst({ return { errors: [e], logs: [], - programMemory: ProgramMemory.empty(), + execState: emptyExecState(), isInterrupted, } } else { @@ -97,7 +108,7 @@ export async function executeAst({ return { logs: [e], errors: [], - programMemory: ProgramMemory.empty(), + execState: emptyExecState(), isInterrupted, } } diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index 29fc9b97e..7cd97d191 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -220,11 +220,11 @@ yo2 = hmm([identifierGuy + 5])` it('should move a binary expression into a new variable', async () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const startIndex = code.indexOf('100 + 100') + 1 const { modifiedAst } = moveValueIntoNewVariable( ast, - programMemory, + execState.memory, [startIndex, startIndex], 'newVar' ) @@ -235,11 +235,11 @@ yo2 = hmm([identifierGuy + 5])` it('should move a value into a new variable', async () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const startIndex = code.indexOf('2.8') + 1 const { modifiedAst } = moveValueIntoNewVariable( ast, - programMemory, + execState.memory, [startIndex, startIndex], 'newVar' ) @@ -250,11 +250,11 @@ yo2 = hmm([identifierGuy + 5])` it('should move a callExpression into a new variable', async () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const startIndex = code.indexOf('def(') const { modifiedAst } = moveValueIntoNewVariable( ast, - programMemory, + execState.memory, [startIndex, startIndex], 'newVar' ) @@ -265,11 +265,11 @@ yo2 = hmm([identifierGuy + 5])` it('should move a binary expression with call expression into a new variable', async () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const startIndex = code.indexOf('jkl(') + 1 const { modifiedAst } = moveValueIntoNewVariable( ast, - programMemory, + execState.memory, [startIndex, startIndex], 'newVar' ) @@ -280,11 +280,11 @@ yo2 = hmm([identifierGuy + 5])` it('should move a identifier into a new variable', async () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const startIndex = code.indexOf('identifierGuy +') + 1 const { modifiedAst } = moveValueIntoNewVariable( ast, - programMemory, + execState.memory, [startIndex, startIndex], 'newVar' ) @@ -465,7 +465,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => { |> line([306.21, 198.87], %)` const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const lineOfInterest = 'line([306.21, 198.85], %, $a)' const range: [number, number] = [ code.indexOf(lineOfInterest), @@ -475,7 +475,7 @@ describe('Testing deleteSegmentFromPipeExpression', () => { const modifiedAst = deleteSegmentFromPipeExpression( [], ast, - programMemory, + execState.memory, code, pathToNode ) @@ -543,7 +543,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${ const code = makeCode(line) const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const lineOfInterest = line const range: [number, number] = [ code.indexOf(lineOfInterest), @@ -554,7 +554,7 @@ ${!replace1 ? ` |> ${line}\n` : ''} |> angledLine([-65, ${ const modifiedAst = deleteSegmentFromPipeExpression( dependentSegments, ast, - programMemory, + execState.memory, code, pathToNode ) @@ -632,7 +632,7 @@ describe('Testing removeSingleConstraintInfo', () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const lineOfInterest = expectedFinish.split('(')[0] + '(' const range: [number, number] = [ code.indexOf(lineOfInterest) + 1, @@ -661,7 +661,7 @@ describe('Testing removeSingleConstraintInfo', () => { pathToNode, argPosition, ast, - programMemory + execState.memory ) if (!mod) return new Error('mod is undefined') const recastCode = recast(mod.modifiedAst) @@ -686,7 +686,7 @@ describe('Testing removeSingleConstraintInfo', () => { const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const lineOfInterest = expectedFinish.split('(')[0] + '(' const range: [number, number] = [ code.indexOf(lineOfInterest) + 1, @@ -711,7 +711,7 @@ describe('Testing removeSingleConstraintInfo', () => { pathToNode, argPosition, ast, - programMemory + execState.memory ) if (!mod) return new Error('mod is undefined') const recastCode = recast(mod.modifiedAst) @@ -882,7 +882,7 @@ sketch002 = startSketchOn({ // const lineOfInterest = 'line([-2.94, 2.7], %)' const ast = parse(codeBefore) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) // deleteFromSelection const range: [number, number] = [ @@ -895,7 +895,7 @@ sketch002 = startSketchOn({ range, type, }, - programMemory, + execState.memory, async () => { await new Promise((resolve) => setTimeout(resolve, 100)) return { diff --git a/src/lang/queryAst.test.ts b/src/lang/queryAst.test.ts index f7bd67fab..f5ca44ab1 100644 --- a/src/lang/queryAst.test.ts +++ b/src/lang/queryAst.test.ts @@ -45,11 +45,11 @@ variableBelowShouldNotBeIncluded = 3 const rangeStart = code.indexOf('// selection-range-7ish-before-this') - 7 const ast = parse(code) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const { variables, bodyPath, insertIndex } = findAllPreviousVariables( ast, - programMemory, + execState.memory, [rangeStart, rangeStart] ) expect(variables).toEqual([ @@ -351,11 +351,11 @@ part001 = startSketchAt([-1.41, 3.46]) const ast = parse(exampleCode) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, selection: { type: 'default', range: [100, 101] }, - programMemory, + programMemory: execState.memory, }) expect(result).toEqual(true) }) @@ -370,11 +370,11 @@ part001 = startSketchAt([-1.41, 3.46]) const ast = parse(exampleCode) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, selection: { type: 'default', range: [100, 101] }, - programMemory, + programMemory: execState.memory, }) expect(result).toEqual(true) }) @@ -383,11 +383,11 @@ part001 = startSketchAt([-1.41, 3.46]) const ast = parse(exampleCode) if (err(ast)) throw ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const result = hasExtrudeSketch({ ast, selection: { type: 'default', range: [10, 11] }, - programMemory, + programMemory: execState.memory, }) expect(result).toEqual(false) }) diff --git a/src/lang/std/sketch.test.ts b/src/lang/std/sketch.test.ts index b3c35e831..37ca212c7 100644 --- a/src/lang/std/sketch.test.ts +++ b/src/lang/std/sketch.test.ts @@ -117,11 +117,11 @@ describe('testing changeSketchArguments', () => { const ast = parse(code) if (err(ast)) return ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const sourceStart = code.indexOf(lineToChange) const changeSketchArgsRetVal = changeSketchArguments( ast, - programMemory, + execState.memory, { type: 'sourceRange', sourceRange: [sourceStart, sourceStart + lineToChange.length], @@ -150,12 +150,12 @@ mySketch001 = startSketchOn('XY') const ast = parse(code) if (err(ast)) return ast - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const sourceStart = code.indexOf(lineToChange) expect(sourceStart).toBe(89) const newSketchLnRetVal = addNewSketchLn({ node: ast, - programMemory, + programMemory: execState.memory, input: { type: 'straight-segment', from: [0, 0], @@ -186,7 +186,7 @@ mySketch001 = startSketchOn('XY') const modifiedAst2 = addCloseToPipe({ node: ast, - programMemory, + programMemory: execState.memory, pathToNode: [ ['body', ''], [0, 'index'], @@ -230,7 +230,7 @@ describe('testing addTagForSketchOnFace', () => { const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const sketchOnFaceRetVal = addTagForSketchOnFace( { - // previousProgramMemory: programMemory, // redundant? + // previousProgramMemory: execState.memory, // redundant? pathToNode, node: ast, }, diff --git a/src/lang/std/sketchConstraints.test.ts b/src/lang/std/sketchConstraints.test.ts index 92e83e57b..08869ae91 100644 --- a/src/lang/std/sketchConstraints.test.ts +++ b/src/lang/std/sketchConstraints.test.ts @@ -34,7 +34,7 @@ async function testingSwapSketchFnCall({ const ast = parse(inputCode) if (err(ast)) return Promise.reject(ast) - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const selections = { codeBasedSelections: [range], otherSelections: [], @@ -45,7 +45,7 @@ async function testingSwapSketchFnCall({ return Promise.reject(new Error('transformInfos undefined')) const ast2 = transformAstSketchLines({ ast, - programMemory, + programMemory: execState.memory, selectionRanges: selections, transformInfos, referenceSegName: '', @@ -360,10 +360,10 @@ part001 = startSketchOn('XY') |> line([2.14, 1.35], %) // normal-segment |> xLine(3.54, %)` it('normal case works', async () => { - const programMemory = await enginelessExecutor(parse(code)) + const execState = await enginelessExecutor(parse(code)) const index = code.indexOf('// normal-segment') - 7 const sg = sketchFromKclValue( - programMemory.get('part001'), + execState.memory.get('part001'), 'part001' ) as Sketch const _segment = getSketchSegmentFromSourceRange(sg, [index, index]) @@ -377,10 +377,10 @@ part001 = startSketchOn('XY') }) }) it('verify it works when the segment is in the `start` property', async () => { - const programMemory = await enginelessExecutor(parse(code)) + const execState = await enginelessExecutor(parse(code)) const index = code.indexOf('// segment-in-start') - 7 const _segment = getSketchSegmentFromSourceRange( - sketchFromKclValue(programMemory.get('part001'), 'part001') as Sketch, + sketchFromKclValue(execState.memory.get('part001'), 'part001') as Sketch, [index, index] ) if (err(_segment)) throw _segment diff --git a/src/lang/std/sketchcombos.test.ts b/src/lang/std/sketchcombos.test.ts index a09093726..c54c4899b 100644 --- a/src/lang/std/sketchcombos.test.ts +++ b/src/lang/std/sketchcombos.test.ts @@ -220,7 +220,7 @@ part001 = startSketchOn('XY') } }) - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const transformInfos = getTransformInfos( makeSelections(selectionRanges.slice(1)), ast, @@ -231,7 +231,7 @@ part001 = startSketchOn('XY') ast, selectionRanges: makeSelections(selectionRanges), transformInfos, - programMemory, + programMemory: execState.memory, }) if (err(newAst)) return Promise.reject(newAst) @@ -311,7 +311,7 @@ part001 = startSketchOn('XY') } }) - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const transformInfos = getTransformInfos( makeSelections(selectionRanges), ast, @@ -322,7 +322,7 @@ part001 = startSketchOn('XY') ast, selectionRanges: makeSelections(selectionRanges), transformInfos, - programMemory, + programMemory: execState.memory, referenceSegName: '', }) if (err(newAst)) return Promise.reject(newAst) @@ -373,7 +373,7 @@ part001 = startSketchOn('XY') } }) - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const transformInfos = getTransformInfos( makeSelections(selectionRanges), ast, @@ -384,7 +384,7 @@ part001 = startSketchOn('XY') ast, selectionRanges: makeSelections(selectionRanges), transformInfos, - programMemory, + programMemory: execState.memory, referenceSegName: '', }) if (err(newAst)) return Promise.reject(newAst) @@ -470,7 +470,7 @@ async function helperThing( } }) - const programMemory = await enginelessExecutor(ast) + const execState = await enginelessExecutor(ast) const transformInfos = getTransformInfos( makeSelections(selectionRanges.slice(1)), ast, @@ -481,7 +481,7 @@ async function helperThing( ast, selectionRanges: makeSelections(selectionRanges), transformInfos, - programMemory, + programMemory: execState.memory, }) if (err(newAst)) return Promise.reject(newAst) diff --git a/src/lang/std/std.test.ts b/src/lang/std/std.test.ts index 5f9d22cdc..c351d0129 100644 --- a/src/lang/std/std.test.ts +++ b/src/lang/std/std.test.ts @@ -17,9 +17,9 @@ describe('testing angledLineThatIntersects', () => { offset: ${offset}, }, %, $yo2) intersect = segEndX(yo2)` - const mem = await enginelessExecutor(parse(code('-1'))) - expect(mem.get('intersect')?.value).toBe(1 + Math.sqrt(2)) + const execState = await enginelessExecutor(parse(code('-1'))) + expect(execState.memory.get('intersect')?.value).toBe(1 + Math.sqrt(2)) const noOffset = await enginelessExecutor(parse(code('0'))) - expect(noOffset.get('intersect')?.value).toBeCloseTo(1) + expect(noOffset.memory.get('intersect')?.value).toBeCloseTo(1) }) }) diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index 59e397d90..f3140cdf3 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -37,6 +37,11 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { DeepPartial } from 'lib/types' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { Sketch } from '../wasm-lib/kcl/bindings/Sketch' +import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' +import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState' +import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory' +import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef' +import { Environment } from '../wasm-lib/kcl/bindings/Environment' export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Expr } from '../wasm-lib/kcl/bindings/Expr' @@ -136,29 +141,46 @@ export const parse = (code: string | Error): Program | Error => { export type PathToNode = [string | number, string][] -interface Memory { - [key: string]: KclValue +export interface ExecState { + memory: ProgramMemory + idGenerator: IdGenerator } -type EnvironmentRef = number +/** + * Create an empty ExecState. This is useful on init to prevent needing an + * Option. + */ +export function emptyExecState(): ExecState { + return { + memory: ProgramMemory.empty(), + idGenerator: defaultIdGenerator(), + } +} + +function execStateFromRaw(raw: RawExecState): ExecState { + return { + memory: ProgramMemory.fromRaw(raw.memory), + idGenerator: raw.idGenerator, + } +} + +export function defaultIdGenerator(): IdGenerator { + return { + nextId: 0, + ids: [], + } +} + +interface Memory { + [key: string]: KclValue | undefined +} const ROOT_ENVIRONMENT_REF: EnvironmentRef = 0 -interface Environment { - bindings: Memory - parent: EnvironmentRef | null -} - function emptyEnvironment(): Environment { return { bindings: {}, parent: null } } -interface RawProgramMemory { - environments: Environment[] - currentEnv: EnvironmentRef - return: KclValue | null -} - /** * This duplicates logic in Rust. The hope is to keep ProgramMemory internals * isolated from the rest of the TypeScript code so that we can move it to Rust @@ -217,7 +239,7 @@ export class ProgramMemory { while (true) { const env = this.environments[envRef] if (env.bindings.hasOwnProperty(name)) { - return env.bindings[name] + return env.bindings[name] ?? null } if (!env.parent) { break @@ -260,6 +282,7 @@ export class ProgramMemory { } for (const [name, value] of Object.entries(env.bindings)) { + if (value === undefined) continue // Check the predicate. if (!predicate(value)) { continue @@ -293,6 +316,7 @@ export class ProgramMemory { while (true) { const env = this.environments[envRef] for (const [name, value] of Object.entries(env.bindings)) { + if (value === undefined) continue // Don't include shadowed variables. if (!map.has(name)) { map.set(name, value) @@ -356,9 +380,10 @@ export function sketchFromKclValue( export const executor = async ( node: Program, programMemory: ProgramMemory | Error = ProgramMemory.empty(), + idGenerator: IdGenerator = defaultIdGenerator(), engineCommandManager: EngineCommandManager, isMock: boolean = false -): Promise => { +): Promise => { if (err(programMemory)) return Promise.reject(programMemory) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -366,6 +391,7 @@ export const executor = async ( const _programMemory = await _executor( node, programMemory, + idGenerator, engineCommandManager, isMock ) @@ -378,9 +404,10 @@ export const executor = async ( export const _executor = async ( node: Program, programMemory: ProgramMemory | Error = ProgramMemory.empty(), + idGenerator: IdGenerator = defaultIdGenerator(), engineCommandManager: EngineCommandManager, isMock: boolean -): Promise => { +): Promise => { if (err(programMemory)) return Promise.reject(programMemory) try { @@ -392,15 +419,16 @@ export const _executor = async ( baseUnit = (await getSettingsState)()?.modeling.defaultUnit.current || 'mm' } - const memory: RawProgramMemory = await execute_wasm( + const execState: RawExecState = await execute_wasm( JSON.stringify(node), JSON.stringify(programMemory.toRaw()), + JSON.stringify(idGenerator), baseUnit, engineCommandManager, fileSystemManager, isMock ) - return ProgramMemory.fromRaw(memory) + return execStateFromRaw(execState) } catch (e: any) { console.log(e) const parsed: RustKclError = JSON.parse(e.toString()) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 39134b6ec..8818979b9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -106,3 +106,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = { /** URL parameter to create a file */ export const CREATE_FILE_URL_PARAM = 'create-file' + +/** Toast id for the app auto-updater toast */ +export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index ad9540810..e93b28a34 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -1,4 +1,11 @@ -import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm' +import { + Program, + ProgramMemory, + _executor, + SourceRange, + ExecState, + defaultIdGenerator, +} from '../lang/wasm' import { EngineCommandManager, EngineCommandManagerEvents, @@ -9,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { err, reportRejection } from 'lib/trap' import { toSync } from './utils' +import { IdGenerator } from 'wasm-lib/kcl/bindings/IdGenerator' type WebSocketResponse = Models['WebSocketResponse_type'] @@ -77,8 +85,9 @@ class MockEngineCommandManager { export async function enginelessExecutor( ast: Program | Error, - pm: ProgramMemory | Error = ProgramMemory.empty() -): Promise { + pm: ProgramMemory | Error = ProgramMemory.empty(), + idGenerator: IdGenerator = defaultIdGenerator() +): Promise { if (err(ast)) return Promise.reject(ast) if (err(pm)) return Promise.reject(pm) @@ -88,15 +97,22 @@ export async function enginelessExecutor( }) as any as EngineCommandManager // eslint-disable-next-line @typescript-eslint/no-floating-promises mockEngineCommandManager.startNewSession() - const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) + const execState = await _executor( + ast, + pm, + idGenerator, + mockEngineCommandManager, + true + ) await mockEngineCommandManager.waitForAllCommands() - return programMemory + return execState } export async function executor( ast: Program, - pm: ProgramMemory = ProgramMemory.empty() -): Promise { + pm: ProgramMemory = ProgramMemory.empty(), + idGenerator: IdGenerator = defaultIdGenerator() +): Promise { const engineCommandManager = new EngineCommandManager() engineCommandManager.start({ setIsStreamReady: () => {}, @@ -117,14 +133,15 @@ export async function executor( toSync(async () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises engineCommandManager.startNewSession() - const programMemory = await _executor( + const execState = await _executor( ast, pm, + idGenerator, engineCommandManager, false ) await engineCommandManager.waitForAllCommands() - resolve(programMemory) + resolve(execState) }, reportRejection) ) }) diff --git a/src/lib/toolbar.ts b/src/lib/toolbar.ts index 6917de76c..988a29d47 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -295,15 +295,24 @@ export const toolbarConfig: Record = { 'break', { id: 'line', - onClick: ({ modelingState, modelingSend }) => - modelingSend({ - type: 'change tool', - data: { - tool: !modelingState.matches({ Sketch: 'Line tool' }) - ? 'line' - : 'none', - }, - }), + onClick: ({ modelingState, modelingSend }) => { + if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) { + // Exit the sketch state if there are no points and they press ESC + modelingSend({ + type: 'Cancel', + }) + } else { + // Exit the tool if there are points and they press ESC + modelingSend({ + type: 'change tool', + data: { + tool: !modelingState.matches({ Sketch: 'Line tool' }) + ? 'line' + : 'none', + }, + }) + } + }, icon: 'line', status: 'available', disabled: (state) => diff --git a/src/lib/useCalculateKclExpression.ts b/src/lib/useCalculateKclExpression.ts index 5f5e044d6..cb195cc6f 100644 --- a/src/lib/useCalculateKclExpression.ts +++ b/src/lib/useCalculateKclExpression.ts @@ -97,7 +97,7 @@ export function useCalculateKclExpression({ }) if (trap(error, { suppress: true })) return } - const { programMemory } = await executeAst({ + const { execState } = await executeAst({ ast, engineCommandManager, useFakeExecutor: true, @@ -111,7 +111,7 @@ export function useCalculateKclExpression({ const init = resultDeclaration?.type === 'VariableDeclaration' && resultDeclaration?.declarations?.[0]?.init - const result = programMemory?.get('__result__')?.value + const result = execState.memory?.get('__result__')?.value setCalcResult(typeof result === 'number' ? String(result) : 'NAN') init && setValueNode(init) } diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index e53700d5a..adbbd1cc0 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -666,6 +666,7 @@ export const modelingMachine = setup({ const testExecute = await executeAst({ ast: modifiedAst, + idGenerator: kclManager.execState.idGenerator, useFakeExecutor: true, engineCommandManager, }) diff --git a/src/main.ts b/src/main.ts index c561910b0..7f8c9d614 100644 --- a/src/main.ts +++ b/src/main.ts @@ -261,10 +261,30 @@ app.on('ready', () => { autoUpdater.checkForUpdates().catch(reportRejection) }, fifteenMinutes) + autoUpdater.on('error', (error) => { + console.error('updater-error', error) + mainWindow?.webContents.send('updater-error', error) + }) + autoUpdater.on('update-available', (info) => { console.log('update-available', info) }) + autoUpdater.prependOnceListener('download-progress', (progress) => { + // For now, we'll send nothing and just start a loading spinner. + // See below for a TODO to send progress data to the renderer. + console.log('update-download-start', { + version: '', + }) + mainWindow?.webContents.send('update-download-start', progress) + }) + + autoUpdater.on('download-progress', (progress) => { + // TODO: in a future PR (https://github.com/KittyCAD/modeling-app/issues/3994) + // send this data to mainWindow to show a progress bar for the download. + console.log('download-progress', progress) + }) + autoUpdater.on('update-downloaded', (info) => { console.log('update-downloaded', info) mainWindow?.webContents.send('update-downloaded', info.version) diff --git a/src/preload.ts b/src/preload.ts index 1f458a424..52e37c1d3 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -16,8 +16,13 @@ const startDeviceFlow = (host: string): Promise => ipcRenderer.invoke('startDeviceFlow', host) const loginWithDeviceFlow = (): Promise => ipcRenderer.invoke('loginWithDeviceFlow') +const onUpdateDownloadStart = ( + callback: (value: { version: string }) => void +) => ipcRenderer.on('update-download-start', (_event, value) => callback(value)) const onUpdateDownloaded = (callback: (value: string) => void) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value)) +const onUpdateError = (callback: (value: Error) => void) => + ipcRenderer.on('update-error', (_event, value) => callback(value)) const appRestart = () => ipcRenderer.invoke('app.restart') const isMac = os.platform() === 'darwin' @@ -144,6 +149,8 @@ contextBridge.exposeInMainWorld('electron', { kittycad, listMachines, getMachineApiIp, + onUpdateDownloadStart, onUpdateDownloaded, + onUpdateError, appRestart, }) diff --git a/src/wasm-lib/Cargo.lock b/src/wasm-lib/Cargo.lock index fb4b537e4..05ff04d4a 100644 --- a/src/wasm-lib/Cargo.lock +++ b/src/wasm-lib/Cargo.lock @@ -434,9 +434,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", diff --git a/src/wasm-lib/derive-docs/src/lib.rs b/src/wasm-lib/derive-docs/src/lib.rs index f57af4b17..590fa5181 100644 --- a/src/wasm-lib/derive-docs/src/lib.rs +++ b/src/wasm-lib/derive-docs/src/lib.rs @@ -753,6 +753,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr let tokens = crate::token::lexer(#code_block).unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())), fs: std::sync::Arc::new(crate::fs::FileManager::new()), @@ -761,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen index 36f40b933..d3cdaa7d5 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_lifetime.gen @@ -5,6 +5,7 @@ mod test_examples_someFn { let tokens = crate::token::lexer("someFn()").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/args_with_refs.gen b/src/wasm-lib/derive-docs/tests/args_with_refs.gen index 93479ac9c..7a5013323 100644 --- a/src/wasm-lib/derive-docs/tests/args_with_refs.gen +++ b/src/wasm-lib/derive-docs/tests/args_with_refs.gen @@ -5,6 +5,7 @@ mod test_examples_someFn { let tokens = crate::token::lexer("someFn()").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_someFn { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/array.gen b/src/wasm-lib/derive-docs/tests/array.gen index 56e02a823..21c2e4b18 100644 --- a/src/wasm-lib/derive-docs/tests/array.gen +++ b/src/wasm-lib/derive-docs/tests/array.gen @@ -5,6 +5,7 @@ mod test_examples_show { let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -38,6 +39,7 @@ mod test_examples_show { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -49,7 +51,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/box.gen b/src/wasm-lib/derive-docs/tests/box.gen index f9692b8ef..18a2ac2af 100644 --- a/src/wasm-lib/derive-docs/tests/box.gen +++ b/src/wasm-lib/derive-docs/tests/box.gen @@ -5,6 +5,7 @@ mod test_examples_show { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen index d171b4c95..7c98fdfb5 100644 --- a/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen +++ b/src/wasm-lib/derive-docs/tests/doc_comment_with_code.gen @@ -6,6 +6,7 @@ mod test_examples_my_func { crate::token::lexer("This is another code block.\nyes sirrr.\nmyFunc").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -17,7 +18,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -39,6 +40,7 @@ mod test_examples_my_func { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmyFunc").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -50,7 +52,7 @@ mod test_examples_my_func { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/lineTo.gen b/src/wasm-lib/derive-docs/tests/lineTo.gen index f066c4161..d46cd45c0 100644 --- a/src/wasm-lib/derive-docs/tests/lineTo.gen +++ b/src/wasm-lib/derive-docs/tests/lineTo.gen @@ -6,6 +6,7 @@ mod test_examples_line_to { crate::token::lexer("This is another code block.\nyes sirrr.\nlineTo").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -17,7 +18,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -39,6 +40,7 @@ mod test_examples_line_to { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nlineTo").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -50,7 +52,7 @@ mod test_examples_line_to { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/min.gen b/src/wasm-lib/derive-docs/tests/min.gen index 8398837af..dfe9681be 100644 --- a/src/wasm-lib/derive-docs/tests/min.gen +++ b/src/wasm-lib/derive-docs/tests/min.gen @@ -5,6 +5,7 @@ mod test_examples_min { let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nmin").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] @@ -38,6 +39,7 @@ mod test_examples_min { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nmin").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -49,7 +51,7 @@ mod test_examples_min { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/option.gen b/src/wasm-lib/derive-docs/tests/option.gen index 666cee018..a6d683cac 100644 --- a/src/wasm-lib/derive-docs/tests/option.gen +++ b/src/wasm-lib/derive-docs/tests/option.gen @@ -5,6 +5,7 @@ mod test_examples_show { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/option_input_format.gen b/src/wasm-lib/derive-docs/tests/option_input_format.gen index f2d5789fd..8e0b43ef2 100644 --- a/src/wasm-lib/derive-docs/tests/option_input_format.gen +++ b/src/wasm-lib/derive-docs/tests/option_input_format.gen @@ -5,6 +5,7 @@ mod test_examples_import { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen index 9c9987ba8..948b7affb 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_box_sketch.gen @@ -5,6 +5,7 @@ mod test_examples_import { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen index fee2b7f97..173c22ee7 100644 --- a/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen +++ b/src/wasm-lib/derive-docs/tests/return_vec_sketch.gen @@ -5,6 +5,7 @@ mod test_examples_import { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nimport").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_import { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/show.gen b/src/wasm-lib/derive-docs/tests/show.gen index 91fec8682..c899b3105 100644 --- a/src/wasm-lib/derive-docs/tests/show.gen +++ b/src/wasm-lib/derive-docs/tests/show.gen @@ -5,6 +5,7 @@ mod test_examples_show { let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_show { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen index 84d99382e..7687cb39e 100644 --- a/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen +++ b/src/wasm-lib/derive-docs/tests/test_args_with_exec_state.gen @@ -5,6 +5,7 @@ mod test_examples_some_function { let tokens = crate::token::lexer("someFunction()").unwrap(); let parser = crate::parser::Parser::new(tokens); let program = parser.ast().unwrap(); + let id_generator = crate::executor::IdGenerator::default(); let ctx = crate::executor::ExecutorContext { engine: std::sync::Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() @@ -16,7 +17,7 @@ mod test_examples_some_function { settings: Default::default(), context_type: crate::executor::ContextType::Mock, }; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 5)] diff --git a/src/wasm-lib/kcl-test-server/src/lib.rs b/src/wasm-lib/kcl-test-server/src/lib.rs index 397a0c8c0..c4e55a518 100644 --- a/src/wasm-lib/kcl-test-server/src/lib.rs +++ b/src/wasm-lib/kcl-test-server/src/lib.rs @@ -166,15 +166,19 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response return bad_request(format!("Parse error: {e}")), }; eprintln!("Executing {test_name}"); + let mut id_generator = kcl_lib::executor::IdGenerator::default(); // This is a shitty source range, I don't know what else to use for it though. // There's no actual KCL associated with this reset_scene call. - if let Err(e) = state.reset_scene(kcl_lib::executor::SourceRange::default()).await { + if let Err(e) = state + .reset_scene(&mut id_generator, kcl_lib::executor::SourceRange::default()) + .await + { return kcl_err(e); } // Let users know if the test is taking a long time. let (done_tx, done_rx) = oneshot::channel::<()>(); let timer = time_until(done_rx); - let snapshot = match state.execute_and_prepare_snapshot(&program).await { + let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await { Ok(sn) => sn, Err(e) => return kcl_err(e), }; diff --git a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs index 589e5de72..7ad3a88c6 100644 --- a/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs +++ b/src/wasm-lib/kcl-to-core/src/conn_mock_core.rs @@ -1,6 +1,9 @@ use anyhow::Result; use indexmap::IndexMap; -use kcl_lib::{errors::KclError, executor::DefaultPlanes}; +use kcl_lib::{ + errors::KclError, + executor::{DefaultPlanes, IdGenerator}, +}; use kittycad_modeling_cmds::{ self as kcmc, id::ModelingCmdId, @@ -357,7 +360,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection { self.batch_end.clone() } - async fn default_planes(&self, source_range: kcl_lib::executor::SourceRange) -> Result { + async fn default_planes( + &self, + id_generator: &mut IdGenerator, + source_range: kcl_lib::executor::SourceRange, + ) -> Result { if NEED_PLANES { { let opt = self.default_planes.read().await.as_ref().cloned(); @@ -366,7 +373,7 @@ impl kcl_lib::engine::EngineManager for EngineConnection { } } // drop the read lock - let new_planes = self.new_default_planes(source_range).await?; + let new_planes = self.new_default_planes(id_generator, source_range).await?; *self.default_planes.write().await = Some(new_planes.clone()); Ok(new_planes) @@ -375,7 +382,11 @@ impl kcl_lib::engine::EngineManager for EngineConnection { } } - async fn clear_scene_post_hook(&self, _source_range: kcl_lib::executor::SourceRange) -> Result<(), KclError> { + async fn clear_scene_post_hook( + &self, + _id_generator: &mut IdGenerator, + _source_range: kcl_lib::executor::SourceRange, + ) -> Result<(), KclError> { Ok(()) } diff --git a/src/wasm-lib/kcl-to-core/src/lib.rs b/src/wasm-lib/kcl-to-core/src/lib.rs index f3a2674b9..b2e6ed699 100644 --- a/src/wasm-lib/kcl-to-core/src/lib.rs +++ b/src/wasm-lib/kcl-to-core/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use kcl_lib::executor::ExecutorContext; +use kcl_lib::executor::{ExecutorContext, IdGenerator}; use std::sync::{Arc, Mutex}; #[cfg(not(target_arch = "wasm32"))] @@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result { settings: Default::default(), context_type: kcl_lib::executor::ContextType::MockCustomForwarded, }; - let _memory = ctx.run(&program, None).await?; + let _memory = ctx.run(&program, None, IdGenerator::default()).await?; let result = result.lock().expect("mutex lock").clone(); Ok(result) diff --git a/src/wasm-lib/kcl/Cargo.toml b/src/wasm-lib/kcl/Cargo.toml index cd4d8193d..626565a8e 100644 --- a/src/wasm-lib/kcl/Cargo.toml +++ b/src/wasm-lib/kcl/Cargo.toml @@ -16,7 +16,7 @@ async-recursion = "1.1.1" async-trait = "0.1.83" base64 = "0.22.1" chrono = "0.4.38" -clap = { version = "4.5.19", default-features = false, optional = true, features = ["std", "derive"] } +clap = { version = "4.5.20", default-features = false, optional = true, features = ["std", "derive"] } convert_case = "0.6.0" dashmap = "6.1.0" databake = { version = "0.1.8", features = ["derive"] } diff --git a/src/wasm-lib/kcl/src/engine/conn.rs b/src/wasm-lib/kcl/src/engine/conn.rs index 286c969e8..0873dc706 100644 --- a/src/wasm-lib/kcl/src/engine/conn.rs +++ b/src/wasm-lib/kcl/src/engine/conn.rs @@ -21,7 +21,7 @@ use tokio_tungstenite::tungstenite::Message as WsMsg; use crate::{ engine::EngineManager, errors::{KclError, KclErrorDetails}, - executor::DefaultPlanes, + executor::{DefaultPlanes, IdGenerator}, }; #[derive(Debug, PartialEq)] @@ -314,7 +314,11 @@ impl EngineManager for EngineConnection { self.batch_end.clone() } - async fn default_planes(&self, source_range: crate::executor::SourceRange) -> Result { + async fn default_planes( + &self, + id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result { { let opt = self.default_planes.read().await.as_ref().cloned(); if let Some(planes) = opt { @@ -322,15 +326,19 @@ impl EngineManager for EngineConnection { } } // drop the read lock - let new_planes = self.new_default_planes(source_range).await?; + let new_planes = self.new_default_planes(id_generator, source_range).await?; *self.default_planes.write().await = Some(new_planes.clone()); Ok(new_planes) } - async fn clear_scene_post_hook(&self, source_range: crate::executor::SourceRange) -> Result<(), KclError> { + async fn clear_scene_post_hook( + &self, + id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result<(), KclError> { // Remake the default planes, since they would have been removed after the scene was cleared. - let new_planes = self.new_default_planes(source_range).await?; + let new_planes = self.new_default_planes(id_generator, source_range).await?; *self.default_planes.write().await = Some(new_planes); Ok(()) diff --git a/src/wasm-lib/kcl/src/engine/conn_mock.rs b/src/wasm-lib/kcl/src/engine/conn_mock.rs index fa951eaba..97f7ecf4c 100644 --- a/src/wasm-lib/kcl/src/engine/conn_mock.rs +++ b/src/wasm-lib/kcl/src/engine/conn_mock.rs @@ -17,7 +17,10 @@ use kcmc::{ }; use kittycad_modeling_cmds::{self as kcmc}; -use crate::{errors::KclError, executor::DefaultPlanes}; +use crate::{ + errors::KclError, + executor::{DefaultPlanes, IdGenerator}, +}; #[derive(Debug, Clone)] pub struct EngineConnection { @@ -44,11 +47,19 @@ impl crate::engine::EngineManager for EngineConnection { self.batch_end.clone() } - async fn default_planes(&self, _source_range: crate::executor::SourceRange) -> Result { + async fn default_planes( + &self, + _id_generator: &mut IdGenerator, + _source_range: crate::executor::SourceRange, + ) -> Result { Ok(DefaultPlanes::default()) } - async fn clear_scene_post_hook(&self, _source_range: crate::executor::SourceRange) -> Result<(), KclError> { + async fn clear_scene_post_hook( + &self, + _id_generator: &mut IdGenerator, + _source_range: crate::executor::SourceRange, + ) -> Result<(), KclError> { Ok(()) } diff --git a/src/wasm-lib/kcl/src/engine/conn_wasm.rs b/src/wasm-lib/kcl/src/engine/conn_wasm.rs index dda6b73c6..8d6f1edaf 100644 --- a/src/wasm-lib/kcl/src/engine/conn_wasm.rs +++ b/src/wasm-lib/kcl/src/engine/conn_wasm.rs @@ -10,7 +10,7 @@ use wasm_bindgen::prelude::*; use crate::{ errors::{KclError, KclErrorDetails}, - executor::DefaultPlanes, + executor::{DefaultPlanes, IdGenerator}, }; #[wasm_bindgen(module = "/../../lang/std/engineConnection.ts")] @@ -68,7 +68,11 @@ impl crate::engine::EngineManager for EngineConnection { self.batch_end.clone() } - async fn default_planes(&self, source_range: crate::executor::SourceRange) -> Result { + async fn default_planes( + &self, + _id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result { // Get the default planes. let promise = self.manager.get_default_planes().map_err(|e| { KclError::Engine(KclErrorDetails { @@ -106,7 +110,11 @@ impl crate::engine::EngineManager for EngineConnection { Ok(default_planes) } - async fn clear_scene_post_hook(&self, source_range: crate::executor::SourceRange) -> Result<(), KclError> { + async fn clear_scene_post_hook( + &self, + _id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result<(), KclError> { self.manager.clear_default_planes().map_err(|e| { KclError::Engine(KclErrorDetails { message: e.to_string().into(), diff --git a/src/wasm-lib/kcl/src/engine/mod.rs b/src/wasm-lib/kcl/src/engine/mod.rs index 900d717ec..792c9699b 100644 --- a/src/wasm-lib/kcl/src/engine/mod.rs +++ b/src/wasm-lib/kcl/src/engine/mod.rs @@ -32,7 +32,7 @@ use uuid::Uuid; use crate::{ errors::{KclError, KclErrorDetails}, - executor::{DefaultPlanes, Point3d}, + executor::{DefaultPlanes, IdGenerator, Point3d}, }; lazy_static::lazy_static! { @@ -52,6 +52,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { /// Get the default planes. async fn default_planes( &self, + id_generator: &mut IdGenerator, _source_range: crate::executor::SourceRange, ) -> Result; @@ -59,6 +60,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { /// (These really only apply to wasm for now). async fn clear_scene_post_hook( &self, + id_generator: &mut IdGenerator, source_range: crate::executor::SourceRange, ) -> Result<(), crate::errors::KclError>; @@ -71,7 +73,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { id_to_source_range: HashMap, ) -> Result; - async fn clear_scene(&self, source_range: crate::executor::SourceRange) -> Result<(), crate::errors::KclError> { + async fn clear_scene( + &self, + id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result<(), crate::errors::KclError> { self.batch_modeling_cmd( uuid::Uuid::new_v4(), source_range, @@ -84,7 +90,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { self.flush_batch(false, source_range).await?; // Do the after clear scene hook. - self.clear_scene_post_hook(source_range).await?; + self.clear_scene_post_hook(id_generator, source_range).await?; Ok(()) } @@ -265,6 +271,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { async fn make_default_plane( &self, + plane_id: uuid::Uuid, x_axis: Point3d, y_axis: Point3d, color: Option, @@ -274,7 +281,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { let default_size = 100.0; let default_origin = Point3d { x: 0.0, y: 0.0, z: 0.0 }.into(); - let plane_id = uuid::Uuid::new_v4(); self.batch_modeling_cmd( plane_id, source_range, @@ -302,11 +308,16 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { Ok(plane_id) } - async fn new_default_planes(&self, source_range: crate::executor::SourceRange) -> Result { - let plane_settings: HashMap)> = HashMap::from([ + async fn new_default_planes( + &self, + id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result { + let plane_settings: HashMap)> = HashMap::from([ ( PlaneName::Xy, ( + id_generator.next_uuid(), Point3d { x: 1.0, y: 0.0, z: 0.0 }, Point3d { x: 0.0, y: 1.0, z: 0.0 }, Some(Color { @@ -320,6 +331,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ( PlaneName::Yz, ( + id_generator.next_uuid(), Point3d { x: 0.0, y: 1.0, z: 0.0 }, Point3d { x: 0.0, y: 0.0, z: 1.0 }, Some(Color { @@ -333,6 +345,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ( PlaneName::Xz, ( + id_generator.next_uuid(), Point3d { x: 1.0, y: 0.0, z: 0.0 }, Point3d { x: 0.0, y: 0.0, z: 1.0 }, Some(Color { @@ -346,6 +359,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ( PlaneName::NegXy, ( + id_generator.next_uuid(), Point3d { x: -1.0, y: 0.0, @@ -358,6 +372,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ( PlaneName::NegYz, ( + id_generator.next_uuid(), Point3d { x: 0.0, y: -1.0, @@ -370,6 +385,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ( PlaneName::NegXz, ( + id_generator.next_uuid(), Point3d { x: -1.0, y: 0.0, @@ -382,10 +398,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { ]); let mut planes = HashMap::new(); - for (name, (x_axis, y_axis, color)) in plane_settings { + for (name, (plane_id, x_axis, y_axis, color)) in plane_settings { planes.insert( name, - self.make_default_plane(x_axis, y_axis, color, source_range).await?, + self.make_default_plane(plane_id, x_axis, y_axis, color, source_range) + .await?, ); } diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index c78f5375e..dfcc90f7d 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -40,6 +40,8 @@ use crate::{ pub struct ExecState { /// Program variable bindings. pub memory: ProgramMemory, + /// The stable artifact ID generator. + pub id_generator: IdGenerator, /// Dynamic state that follows dynamic flow of the program. pub dynamic_state: DynamicState, /// The current value of the pipe operator returned from the previous @@ -292,7 +294,34 @@ impl DynamicState { } } -/// A memory item. +/// A generator for ArtifactIds that can be stable across executions. +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct IdGenerator { + next_id: usize, + ids: Vec, +} + +impl IdGenerator { + pub fn new() -> Self { + Self::default() + } + + pub fn next_uuid(&mut self) -> uuid::Uuid { + if let Some(id) = self.ids.get(self.next_id) { + self.next_id += 1; + *id + } else { + let id = uuid::Uuid::new_v4(); + self.ids.push(id); + self.next_id += 1; + id + } + } +} + +/// Any KCL value. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] #[serde(tag = "type")] @@ -599,6 +628,82 @@ pub struct Plane { pub meta: Vec, } +impl Plane { + pub(crate) fn from_plane_data(value: crate::std::sketch::PlaneData, exec_state: &mut ExecState) -> Self { + let id = exec_state.id_generator.next_uuid(); + match value { + crate::std::sketch::PlaneData::XY => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(1.0, 0.0, 0.0), + y_axis: Point3d::new(0.0, 1.0, 0.0), + z_axis: Point3d::new(0.0, 0.0, 1.0), + value: PlaneType::XY, + meta: vec![], + }, + crate::std::sketch::PlaneData::NegXY => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(1.0, 0.0, 0.0), + y_axis: Point3d::new(0.0, 1.0, 0.0), + z_axis: Point3d::new(0.0, 0.0, -1.0), + value: PlaneType::XY, + meta: vec![], + }, + crate::std::sketch::PlaneData::XZ => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(1.0, 0.0, 0.0), + y_axis: Point3d::new(0.0, 0.0, 1.0), + z_axis: Point3d::new(0.0, -1.0, 0.0), + value: PlaneType::XZ, + meta: vec![], + }, + crate::std::sketch::PlaneData::NegXZ => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(-1.0, 0.0, 0.0), + y_axis: Point3d::new(0.0, 0.0, 1.0), + z_axis: Point3d::new(0.0, 1.0, 0.0), + value: PlaneType::XZ, + meta: vec![], + }, + crate::std::sketch::PlaneData::YZ => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(0.0, 1.0, 0.0), + y_axis: Point3d::new(0.0, 0.0, 1.0), + z_axis: Point3d::new(1.0, 0.0, 0.0), + value: PlaneType::YZ, + meta: vec![], + }, + crate::std::sketch::PlaneData::NegYZ => Plane { + id, + origin: Point3d::new(0.0, 0.0, 0.0), + x_axis: Point3d::new(0.0, 1.0, 0.0), + y_axis: Point3d::new(0.0, 0.0, 1.0), + z_axis: Point3d::new(-1.0, 0.0, 0.0), + value: PlaneType::YZ, + meta: vec![], + }, + crate::std::sketch::PlaneData::Plane { + origin, + x_axis, + y_axis, + z_axis, + } => Plane { + id, + origin: *origin, + x_axis: *x_axis, + y_axis: *y_axis, + z_axis: *z_axis, + value: PlaneType::Custom, + meta: vec![], + }, + } + } +} + #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -1836,8 +1941,12 @@ impl ExecutorContext { Ok(ctx) } - pub async fn reset_scene(&self, source_range: crate::executor::SourceRange) -> Result<()> { - self.engine.clear_scene(source_range).await?; + pub async fn reset_scene( + &self, + id_generator: &mut IdGenerator, + source_range: crate::executor::SourceRange, + ) -> Result<()> { + self.engine.clear_scene(id_generator, source_range).await?; Ok(()) } @@ -1848,8 +1957,11 @@ impl ExecutorContext { &self, program: &crate::ast::types::Program, memory: Option, + id_generator: IdGenerator, ) -> Result { - self.run_with_session_data(program, memory).await.map(|x| x.0) + self.run_with_session_data(program, memory, id_generator) + .await + .map(|x| x.0) } /// Perform the execution of a program. /// You can optionally pass in some initialization memory. @@ -1858,11 +1970,22 @@ impl ExecutorContext { &self, program: &crate::ast::types::Program, memory: Option, + id_generator: IdGenerator, ) -> Result<(ExecState, Option), KclError> { + let memory = if let Some(memory) = memory { + memory.clone() + } else { + Default::default() + }; + let mut exec_state = ExecState { + memory, + id_generator, + ..Default::default() + }; // Before we even start executing the program, set the units. self.engine .batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), SourceRange::default(), &ModelingCmd::from(mcmd::SetSceneUnits { unit: match self.settings.units { @@ -1876,15 +1999,7 @@ impl ExecutorContext { }), ) .await?; - let memory = if let Some(memory) = memory { - memory.clone() - } else { - Default::default() - }; - let mut exec_state = ExecState { - memory, - ..Default::default() - }; + self.inner_execute(program, &mut exec_state, crate::executor::BodyType::Root) .await?; let session_data = self.engine.get_session_data(); @@ -2029,8 +2144,12 @@ impl ExecutorContext { } /// Execute the program, then get a PNG screenshot. - pub async fn execute_and_prepare_snapshot(&self, program: &Program) -> Result { - let _ = self.run(program, None).await?; + pub async fn execute_and_prepare_snapshot( + &self, + program: &Program, + id_generator: IdGenerator, + ) -> Result { + let _ = self.run(program, None, id_generator).await?; // Zoom to fit. self.engine @@ -2175,7 +2294,7 @@ mod tests { settings: Default::default(), context_type: ContextType::Mock, }; - let exec_state = ctx.run(&program, None).await?; + let exec_state = ctx.run(&program, None, IdGenerator::default()).await?; Ok(exec_state.memory) } diff --git a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs index 45215c096..31a393dcc 100644 --- a/src/wasm-lib/kcl/src/lsp/kcl/mod.rs +++ b/src/wasm-lib/kcl/src/lsp/kcl/mod.rs @@ -41,7 +41,7 @@ use tower_lsp::{ use crate::{ ast::types::{Expr, VariableKind}, - executor::SourceRange, + executor::{IdGenerator, SourceRange}, lsp::{backend::Backend as _, util::IntoDiagnostic}, parser::PIPE_OPERATOR, token::TokenType, @@ -588,10 +588,15 @@ impl Backend { return Ok(()); } - // Clear the scene, before we execute so it's not fugly as shit. - executor_ctx.engine.clear_scene(SourceRange::default()).await?; + let mut id_generator = IdGenerator::default(); - let exec_state = match executor_ctx.run(ast, None).await { + // Clear the scene, before we execute so it's not fugly as shit. + executor_ctx + .engine + .clear_scene(&mut id_generator, SourceRange::default()) + .await?; + + let exec_state = match executor_ctx.run(ast, None, id_generator).await { Ok(exec_state) => exec_state, Err(err) => { self.memory_map.remove(params.uri.as_str()); diff --git a/src/wasm-lib/kcl/src/std/chamfer.rs b/src/wasm-lib/kcl/src/std/chamfer.rs index b58c08d8e..e3deb4250 100644 --- a/src/wasm-lib/kcl/src/std/chamfer.rs +++ b/src/wasm-lib/kcl/src/std/chamfer.rs @@ -133,7 +133,7 @@ async fn inner_chamfer( EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id, }; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_end_cmd( id, ModelingCmd::from(mcmd::Solid3dFilletEdge { diff --git a/src/wasm-lib/kcl/src/std/extrude.rs b/src/wasm-lib/kcl/src/std/extrude.rs index d130e9b70..a8724fba9 100644 --- a/src/wasm-lib/kcl/src/std/extrude.rs +++ b/src/wasm-lib/kcl/src/std/extrude.rs @@ -18,10 +18,10 @@ use crate::{ }; /// Extrudes by a given amount. -pub async fn extrude(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result { let (length, sketch_set) = args.get_number_sketch_set()?; - let result = inner_extrude(length, sketch_set, args).await?; + let result = inner_extrude(length, sketch_set, exec_state, args).await?; Ok(result.into()) } @@ -75,8 +75,13 @@ pub async fn extrude(_exec_state: &mut ExecState, args: Args) -> Result Result { - let id = uuid::Uuid::new_v4(); +async fn inner_extrude( + length: f64, + sketch_set: SketchSet, + exec_state: &mut ExecState, + args: Args, +) -> Result { + let id = exec_state.id_generator.next_uuid(); // Extrude the element(s). let sketches: Vec = sketch_set.into(); @@ -85,7 +90,7 @@ async fn inner_extrude(length: f64, sketch_set: SketchSet, args: Args) -> Result // Before we extrude, we need to enable the sketch mode. // We do this here in case extrude is called out of order. args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::EnableSketchMode { animated: false, ortho: false, @@ -112,21 +117,26 @@ async fn inner_extrude(length: f64, sketch_set: SketchSet, args: Args) -> Result // Disable the sketch mode. args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable {}), ) .await?; - solids.push(do_post_extrude(sketch.clone(), length, args.clone()).await?); + solids.push(do_post_extrude(sketch.clone(), length, exec_state, args.clone()).await?); } Ok(solids.into()) } -pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> Result, KclError> { +pub(crate) async fn do_post_extrude( + sketch: Sketch, + length: f64, + exec_state: &mut ExecState, + args: Args, +) -> Result, KclError> { // Bring the object to the front of the scene. // See: https://github.com/KittyCAD/modeling-app/issues/806 args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }), ) .await?; @@ -159,7 +169,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> let solid3d_info = args .send_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo { edge_id, object_id: sketch.id, @@ -192,7 +202,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> // Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm) // uses this to build the artifact graph, which the UI needs. args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid3dGetOppositeEdge { edge_id: curve_id, object_id: sketch.id, @@ -202,7 +212,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> .await?; args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge { edge_id: curve_id, object_id: sketch.id, @@ -216,7 +226,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> sides: face_id_map, start_cap_id, end_cap_id, - } = analyze_faces(&args, face_infos); + } = analyze_faces(exec_state, &args, face_infos); // Iterate over the sketch.value array and add face_id to GeoMeta let new_value = sketch .value @@ -252,7 +262,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) -> let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { // pushing this values with a fake face_id to make extrudes mock-execute safe - face_id: Uuid::new_v4(), + face_id: exec_state.id_generator.next_uuid(), tag: path.get_base().tag.clone(), geo_meta: GeoMeta { id: path.get_base().geo_meta.id, @@ -291,15 +301,15 @@ struct Faces { start_cap_id: Option, } -fn analyze_faces(args: &Args, face_infos: Vec) -> Faces { +fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec) -> Faces { let mut faces = Faces { sides: HashMap::with_capacity(face_infos.len()), ..Default::default() }; if args.ctx.is_mock() { // Create fake IDs for start and end caps, to make extrudes mock-execute safe - faces.start_cap_id = Some(Uuid::new_v4()); - faces.end_cap_id = Some(Uuid::new_v4()); + faces.start_cap_id = Some(exec_state.id_generator.next_uuid()); + faces.end_cap_id = Some(exec_state.id_generator.next_uuid()); } for face_info in face_infos { match face_info.cap { diff --git a/src/wasm-lib/kcl/src/std/fillet.rs b/src/wasm-lib/kcl/src/std/fillet.rs index d238e9ccc..8e0f0fdc9 100644 --- a/src/wasm-lib/kcl/src/std/fillet.rs +++ b/src/wasm-lib/kcl/src/std/fillet.rs @@ -142,7 +142,7 @@ async fn inner_fillet( for edge_tag in data.tags { let edge_id = edge_tag.get_engine_id(exec_state, &args)?; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_end_cmd( id, ModelingCmd::from(mcmd::Solid3dFilletEdge { @@ -229,15 +229,16 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result }] async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result { if args.ctx.is_mock() { - return Ok(Uuid::new_v4()); + return Ok(exec_state.id_generator.next_uuid()); } let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?; + let id = exec_state.id_generator.next_uuid(); let tagged_path = args.get_tag_engine_info(exec_state, &tag)?; let resp = args .send_modeling_cmd( - uuid::Uuid::new_v4(), + id, ModelingCmd::from(mcmd::Solid3dGetOppositeEdge { edge_id: tagged_path.id, object_id: tagged_path.sketch, @@ -310,15 +311,16 @@ async fn inner_get_next_adjacent_edge( args: Args, ) -> Result { if args.ctx.is_mock() { - return Ok(Uuid::new_v4()); + return Ok(exec_state.id_generator.next_uuid()); } let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?; + let id = exec_state.id_generator.next_uuid(); let tagged_path = args.get_tag_engine_info(exec_state, &tag)?; let resp = args .send_modeling_cmd( - uuid::Uuid::new_v4(), + id, ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge { edge_id: tagged_path.id, object_id: tagged_path.sketch, @@ -399,15 +401,16 @@ async fn inner_get_previous_adjacent_edge( args: Args, ) -> Result { if args.ctx.is_mock() { - return Ok(Uuid::new_v4()); + return Ok(exec_state.id_generator.next_uuid()); } let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?; + let id = exec_state.id_generator.next_uuid(); let tagged_path = args.get_tag_engine_info(exec_state, &tag)?; let resp = args .send_modeling_cmd( - uuid::Uuid::new_v4(), + id, ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge { edge_id: tagged_path.id, object_id: tagged_path.sketch, diff --git a/src/wasm-lib/kcl/src/std/helix.rs b/src/wasm-lib/kcl/src/std/helix.rs index d62696fde..5d340a5fb 100644 --- a/src/wasm-lib/kcl/src/std/helix.rs +++ b/src/wasm-lib/kcl/src/std/helix.rs @@ -32,10 +32,10 @@ pub struct HelixData { } /// Create a helix on a cylinder. -pub async fn helix(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result { let (data, solid): (HelixData, Box) = args.get_data_and_solid()?; - let solid = inner_helix(data, solid, args).await?; + let solid = inner_helix(data, solid, exec_state, args).await?; Ok(KclValue::Solid(solid)) } @@ -54,8 +54,13 @@ pub async fn helix(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result, KclError> { - let id = uuid::Uuid::new_v4(); +async fn inner_helix( + data: HelixData, + solid: Box, + exec_state: &mut ExecState, + args: Args, +) -> Result, KclError> { + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, ModelingCmd::from(mcmd::EntityMakeHelix { diff --git a/src/wasm-lib/kcl/src/std/import.rs b/src/wasm-lib/kcl/src/std/import.rs index 13155475a..3167c7fc8 100644 --- a/src/wasm-lib/kcl/src/std/import.rs +++ b/src/wasm-lib/kcl/src/std/import.rs @@ -126,10 +126,10 @@ impl From for InputFormat { /// /// Import paths are relative to the current project directory. This only works in the desktop app /// not in browser. -pub async fn import(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn import(exec_state: &mut ExecState, args: Args) -> Result { let (file_path, options): (String, Option) = args.get_import_data()?; - let imported_geometry = inner_import(file_path, options, args).await?; + let imported_geometry = inner_import(file_path, options, exec_state, args).await?; Ok(KclValue::ImportedGeometry(imported_geometry)) } @@ -170,6 +170,7 @@ pub async fn import(_exec_state: &mut ExecState, args: Args) -> Result, + exec_state: &mut ExecState, args: Args, ) -> Result { if file_path.is_empty() { @@ -286,13 +287,13 @@ async fn inner_import( if args.ctx.is_mock() { return Ok(ImportedGeometry { - id: uuid::Uuid::new_v4(), + id: exec_state.id_generator.next_uuid(), value: import_files.iter().map(|f| f.path.to_string()).collect(), meta: vec![args.source_range.into()], }); } - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); let resp = args .send_modeling_cmd( id, diff --git a/src/wasm-lib/kcl/src/std/loft.rs b/src/wasm-lib/kcl/src/std/loft.rs index 37c8cb6a4..52e70b884 100644 --- a/src/wasm-lib/kcl/src/std/loft.rs +++ b/src/wasm-lib/kcl/src/std/loft.rs @@ -50,10 +50,10 @@ impl Default for LoftData { } /// Create a 3D surface or solid by interpolating between two or more sketches. -pub async fn loft(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result { let (sketches, data): (Vec, Option) = args.get_sketches_and_data()?; - let solid = inner_loft(sketches, data, args).await?; + let solid = inner_loft(sketches, data, exec_state, args).await?; Ok(KclValue::Solid(solid)) } @@ -135,7 +135,12 @@ pub async fn loft(_exec_state: &mut ExecState, args: Args) -> Result, data: Option, args: Args) -> Result, KclError> { +async fn inner_loft( + sketches: Vec, + data: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result, KclError> { // Make sure we have at least two sketches. if sketches.len() < 2 { return Err(KclError::Semantic(KclErrorDetails { @@ -150,7 +155,7 @@ async fn inner_loft(sketches: Vec, data: Option, args: Args) - // Get the loft data. let data = data.unwrap_or_default(); - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, ModelingCmd::from(mcmd::Loft { @@ -166,5 +171,5 @@ async fn inner_loft(sketches: Vec, data: Option, args: Args) - .await?; // Using the first sketch as the base curve, idk we might want to change this later. - do_post_extrude(sketches[0].clone(), 0.0, args).await + do_post_extrude(sketches[0].clone(), 0.0, exec_state, args).await } diff --git a/src/wasm-lib/kcl/src/std/mirror.rs b/src/wasm-lib/kcl/src/std/mirror.rs index 92a17c2f2..21ff9d50e 100644 --- a/src/wasm-lib/kcl/src/std/mirror.rs +++ b/src/wasm-lib/kcl/src/std/mirror.rs @@ -121,7 +121,7 @@ async fn inner_mirror_2d( let (axis, origin) = axis.axis_and_origin()?; args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::EntityMirror { ids: starting_sketches.iter().map(|sketch| sketch.id).collect(), axis, @@ -134,7 +134,7 @@ async fn inner_mirror_2d( let edge_id = edge.get_engine_id(exec_state, &args)?; args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::EntityMirrorAcrossEdge { ids: starting_sketches.iter().map(|sketch| sketch.id).collect(), edge_id, diff --git a/src/wasm-lib/kcl/src/std/patterns.rs b/src/wasm-lib/kcl/src/std/patterns.rs index 9631d218e..6a9b85fab 100644 --- a/src/wasm-lib/kcl/src/std/patterns.rs +++ b/src/wasm-lib/kcl/src/std/patterns.rs @@ -296,7 +296,7 @@ async fn inner_pattern_transform<'a>( let mut solids = Vec::new(); for e in starting_solids { - let new_solids = send_pattern_transform(transform.clone(), &e, args).await?; + let new_solids = send_pattern_transform(transform.clone(), &e, exec_state, args).await?; solids.extend(new_solids); } Ok(solids) @@ -307,9 +307,10 @@ async fn send_pattern_transform( // https://github.com/KittyCAD/modeling-app/issues/2821 transform: Vec, solid: &Solid, + exec_state: &mut ExecState, args: &Args, ) -> Result>, KclError> { - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); let resp = args .send_modeling_cmd( @@ -473,7 +474,7 @@ mod tests { } /// A linear pattern on a 2D sketch. -pub async fn pattern_linear_2d(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch_set): (LinearPattern2dData, SketchSet) = args.get_data_and_sketch_set()?; if data.axis == [0.0, 0.0] { @@ -485,7 +486,7 @@ pub async fn pattern_linear_2d(_exec_state: &mut ExecState, args: Args) -> Resul })); } - let sketches = inner_pattern_linear_2d(data, sketch_set, args).await?; + let sketches = inner_pattern_linear_2d(data, sketch_set, exec_state, args).await?; Ok(sketches.into()) } @@ -509,6 +510,7 @@ pub async fn pattern_linear_2d(_exec_state: &mut ExecState, args: Args) -> Resul async fn inner_pattern_linear_2d( data: LinearPattern2dData, sketch_set: SketchSet, + exec_state: &mut ExecState, args: Args, ) -> Result>, KclError> { let starting_sketches: Vec> = sketch_set.into(); @@ -522,6 +524,7 @@ async fn inner_pattern_linear_2d( let geometries = pattern_linear( LinearPattern::TwoD(data.clone()), Geometry::Sketch(sketch.clone()), + exec_state, args.clone(), ) .await?; @@ -600,6 +603,7 @@ async fn inner_pattern_linear_3d( let geometries = pattern_linear( LinearPattern::ThreeD(data.clone()), Geometry::Solid(solid.clone()), + exec_state, args.clone(), ) .await?; @@ -617,8 +621,13 @@ async fn inner_pattern_linear_3d( Ok(solids) } -async fn pattern_linear(data: LinearPattern, geometry: Geometry, args: Args) -> Result { - let id = uuid::Uuid::new_v4(); +async fn pattern_linear( + data: LinearPattern, + geometry: Geometry, + exec_state: &mut ExecState, + args: Args, +) -> Result { + let id = exec_state.id_generator.next_uuid(); let resp = args .send_modeling_cmd( @@ -745,10 +754,10 @@ impl CircularPattern { } /// A circular pattern on a 2D sketch. -pub async fn pattern_circular_2d(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch_set): (CircularPattern2dData, SketchSet) = args.get_data_and_sketch_set()?; - let sketches = inner_pattern_circular_2d(data, sketch_set, args).await?; + let sketches = inner_pattern_circular_2d(data, sketch_set, exec_state, args).await?; Ok(sketches.into()) } @@ -779,6 +788,7 @@ pub async fn pattern_circular_2d(_exec_state: &mut ExecState, args: Args) -> Res async fn inner_pattern_circular_2d( data: CircularPattern2dData, sketch_set: SketchSet, + exec_state: &mut ExecState, args: Args, ) -> Result>, KclError> { let starting_sketches: Vec> = sketch_set.into(); @@ -792,6 +802,7 @@ async fn inner_pattern_circular_2d( let geometries = pattern_circular( CircularPattern::TwoD(data.clone()), Geometry::Sketch(sketch.clone()), + exec_state, args.clone(), ) .await?; @@ -861,6 +872,7 @@ async fn inner_pattern_circular_3d( let geometries = pattern_circular( CircularPattern::ThreeD(data.clone()), Geometry::Solid(solid.clone()), + exec_state, args.clone(), ) .await?; @@ -878,8 +890,13 @@ async fn inner_pattern_circular_3d( Ok(solids) } -async fn pattern_circular(data: CircularPattern, geometry: Geometry, args: Args) -> Result { - let id = uuid::Uuid::new_v4(); +async fn pattern_circular( + data: CircularPattern, + geometry: Geometry, + exec_state: &mut ExecState, + args: Args, +) -> Result { + let id = exec_state.id_generator.next_uuid(); let center = data.center(); let resp = args diff --git a/src/wasm-lib/kcl/src/std/planes.rs b/src/wasm-lib/kcl/src/std/planes.rs index e101f5450..436d19938 100644 --- a/src/wasm-lib/kcl/src/std/planes.rs +++ b/src/wasm-lib/kcl/src/std/planes.rs @@ -48,10 +48,10 @@ impl From for PlaneData { } /// Offset a plane by a distance along its normal. -pub async fn offset_plane(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result { let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?; - let plane = inner_offset_plane(std_plane, offset).await?; + let plane = inner_offset_plane(std_plane, offset, exec_state).await?; Ok(KclValue::UserVal(UserVal::new( vec![Metadata { @@ -132,11 +132,15 @@ pub async fn offset_plane(_exec_state: &mut ExecState, args: Args) -> Result Result { +async fn inner_offset_plane( + std_plane: StandardPlane, + offset: f64, + exec_state: &mut ExecState, +) -> Result { // Convert to the plane type. let plane_data: PlaneData = std_plane.into(); // Convert to a plane. - let mut plane = Plane::from(plane_data); + let mut plane = Plane::from_plane_data(plane_data, exec_state); match std_plane { StandardPlane::XY => { diff --git a/src/wasm-lib/kcl/src/std/revolve.rs b/src/wasm-lib/kcl/src/std/revolve.rs index 701eff1d8..5ba5df96b 100644 --- a/src/wasm-lib/kcl/src/std/revolve.rs +++ b/src/wasm-lib/kcl/src/std/revolve.rs @@ -263,7 +263,7 @@ async fn inner_revolve( let angle = Angle::from_degrees(data.angle.unwrap_or(360.0)); - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); match data.axis { AxisOrEdgeReference::Axis(axis) => { let (axis, origin) = axis.axis_and_origin()?; @@ -295,7 +295,7 @@ async fn inner_revolve( } } - do_post_extrude(sketch, 0.0, args).await + do_post_extrude(sketch, 0.0, exec_state, args).await } #[cfg(test)] diff --git a/src/wasm-lib/kcl/src/std/shapes.rs b/src/wasm-lib/kcl/src/std/shapes.rs index f0ac60165..96feb777e 100644 --- a/src/wasm-lib/kcl/src/std/shapes.rs +++ b/src/wasm-lib/kcl/src/std/shapes.rs @@ -97,7 +97,7 @@ async fn inner_circle( let angle_start = Angle::zero(); let angle_end = Angle::turn(); - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, diff --git a/src/wasm-lib/kcl/src/std/shell.rs b/src/wasm-lib/kcl/src/std/shell.rs index 80f0c4977..16162beb2 100644 --- a/src/wasm-lib/kcl/src/std/shell.rs +++ b/src/wasm-lib/kcl/src/std/shell.rs @@ -229,7 +229,7 @@ async fn inner_shell( } args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid3dShellFace { hollow: false, face_ids, @@ -314,7 +314,7 @@ async fn inner_hollow( args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?; args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid3dShellFace { hollow: true, face_ids: Vec::new(), // This is empty because we want to hollow the entire object. diff --git a/src/wasm-lib/kcl/src/std/sketch.rs b/src/wasm-lib/kcl/src/std/sketch.rs index c725e4cd4..2d0b622ef 100644 --- a/src/wasm-lib/kcl/src/std/sketch.rs +++ b/src/wasm-lib/kcl/src/std/sketch.rs @@ -16,8 +16,8 @@ use crate::{ ast::types::TagDeclarator, errors::{KclError, KclErrorDetails}, executor::{ - BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, PlaneType, Point2d, Point3d, Sketch, SketchSet, - SketchSurface, Solid, TagEngineInfo, TagIdentifier, UserVal, + BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface, + Solid, TagEngineInfo, TagIdentifier, UserVal, }, std::{ utils::{ @@ -93,10 +93,10 @@ pub enum StartOrEnd { } /// Draw a line to a point. -pub async fn line_to(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn line_to(exec_state: &mut ExecState, args: Args) -> Result { let (to, sketch, tag): ([f64; 2], Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_line_to(to, sketch, tag, args).await?; + let new_sketch = inner_line_to(to, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -119,10 +119,11 @@ async fn inner_line_to( to: [f64; 2], sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, @@ -159,10 +160,10 @@ async fn inner_line_to( } /// Draw a line to a point on the x-axis. -pub async fn x_line_to(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn x_line_to(exec_state: &mut ExecState, args: Args) -> Result { let (to, sketch, tag): (f64, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_x_line_to(to, sketch, tag, args).await?; + let new_sketch = inner_x_line_to(to, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -192,19 +193,25 @@ pub async fn x_line_to(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result { +async fn inner_x_line_to( + to: f64, + sketch: Sketch, + tag: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result { let from = sketch.current_pen_position()?; - let new_sketch = inner_line_to([to, from.y], sketch, tag, args).await?; + let new_sketch = inner_line_to([to, from.y], sketch, tag, exec_state, args).await?; Ok(new_sketch) } /// Draw a line to a point on the y-axis. -pub async fn y_line_to(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn y_line_to(exec_state: &mut ExecState, args: Args) -> Result { let (to, sketch, tag): (f64, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_y_line_to(to, sketch, tag, args).await?; + let new_sketch = inner_y_line_to(to, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -227,18 +234,24 @@ pub async fn y_line_to(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result { +async fn inner_y_line_to( + to: f64, + sketch: Sketch, + tag: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result { let from = sketch.current_pen_position()?; - let new_sketch = inner_line_to([from.x, to], sketch, tag, args).await?; + let new_sketch = inner_line_to([from.x, to], sketch, tag, exec_state, args).await?; Ok(new_sketch) } /// Draw a line. -pub async fn line(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn line(exec_state: &mut ExecState, args: Args) -> Result { let (delta, sketch, tag): ([f64; 2], Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_line(delta, sketch, tag, args).await?; + let new_sketch = inner_line(delta, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -273,12 +286,13 @@ async fn inner_line( delta: [f64; 2], sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; let to = [from.x + delta[0], from.y + delta[1]]; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, @@ -315,10 +329,10 @@ async fn inner_line( } /// Draw a line on the x-axis. -pub async fn x_line(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result { let (length, sketch, tag): (f64, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_x_line(length, sketch, tag, args).await?; + let new_sketch = inner_x_line(length, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -347,15 +361,21 @@ pub async fn x_line(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result { - inner_line([length, 0.0], sketch, tag, args).await +async fn inner_x_line( + length: f64, + sketch: Sketch, + tag: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result { + inner_line([length, 0.0], sketch, tag, exec_state, args).await } /// Draw a line on the y-axis. -pub async fn y_line(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result { let (length, sketch, tag): (f64, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_y_line(length, sketch, tag, args).await?; + let new_sketch = inner_y_line(length, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -379,8 +399,14 @@ pub async fn y_line(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result { - inner_line([0.0, length], sketch, tag, args).await +async fn inner_y_line( + length: f64, + sketch: Sketch, + tag: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result { + inner_line([0.0, length], sketch, tag, exec_state, args).await } /// Data to draw an angled line. @@ -400,10 +426,10 @@ pub enum AngledLineData { } /// Draw an angled line. -pub async fn angled_line(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (AngledLineData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_angled_line(data, sketch, tag, args).await?; + let new_sketch = inner_angled_line(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -431,6 +457,7 @@ async fn inner_angled_line( data: AngledLineData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; @@ -448,7 +475,7 @@ async fn inner_angled_line( let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]]; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, @@ -484,10 +511,10 @@ async fn inner_angled_line( } /// Draw an angled line of a given x length. -pub async fn angled_line_of_x_length(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn angled_line_of_x_length(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (AngledLineData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, args).await?; + let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -511,6 +538,7 @@ async fn inner_angled_line_of_x_length( data: AngledLineData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let (angle, length) = match data { @@ -534,7 +562,7 @@ async fn inner_angled_line_of_x_length( let to = get_y_component(Angle::from_degrees(angle), length); - let new_sketch = inner_line(to.into(), sketch, tag, args).await?; + let new_sketch = inner_line(to.into(), sketch, tag, exec_state, args).await?; Ok(new_sketch) } @@ -551,10 +579,10 @@ pub struct AngledLineToData { } /// Draw an angled line to a given x coordinate. -pub async fn angled_line_to_x(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn angled_line_to_x(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (AngledLineToData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_angled_line_to_x(data, sketch, tag, args).await?; + let new_sketch = inner_angled_line_to_x(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -579,6 +607,7 @@ async fn inner_angled_line_to_x( data: AngledLineToData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; @@ -602,15 +631,15 @@ async fn inner_angled_line_to_x( let y_component = x_component * f64::tan(angle.to_radians()); let y_to = from.y + y_component; - let new_sketch = inner_line_to([x_to, y_to], sketch, tag, args).await?; + let new_sketch = inner_line_to([x_to, y_to], sketch, tag, exec_state, args).await?; Ok(new_sketch) } /// Draw an angled line of a given y length. -pub async fn angled_line_of_y_length(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn angled_line_of_y_length(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (AngledLineData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, args).await?; + let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -637,6 +666,7 @@ async fn inner_angled_line_of_y_length( data: AngledLineData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let (angle, length) = match data { @@ -660,16 +690,16 @@ async fn inner_angled_line_of_y_length( let to = get_x_component(Angle::from_degrees(angle), length); - let new_sketch = inner_line(to.into(), sketch, tag, args).await?; + let new_sketch = inner_line(to.into(), sketch, tag, exec_state, args).await?; Ok(new_sketch) } /// Draw an angled line to a given y coordinate. -pub async fn angled_line_to_y(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn angled_line_to_y(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (AngledLineToData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_angled_line_to_y(data, sketch, tag, args).await?; + let new_sketch = inner_angled_line_to_y(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -694,6 +724,7 @@ async fn inner_angled_line_to_y( data: AngledLineToData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; @@ -717,7 +748,7 @@ async fn inner_angled_line_to_y( let x_component = y_component / f64::tan(angle.to_radians()); let x_to = from.x + x_component; - let new_sketch = inner_line_to([x_to, y_to], sketch, tag, args).await?; + let new_sketch = inner_line_to([x_to, y_to], sketch, tag, exec_state, args).await?; Ok(new_sketch) } @@ -788,7 +819,7 @@ async fn inner_angled_line_that_intersects( from, ); - let new_sketch = inner_line_to(to.into(), sketch, tag, args).await?; + let new_sketch = inner_line_to(to.into(), sketch, tag, exec_state, args).await?; Ok(new_sketch) } @@ -891,82 +922,6 @@ pub enum PlaneData { }, } -impl From for Plane { - fn from(value: PlaneData) -> Self { - let id = uuid::Uuid::new_v4(); - match value { - PlaneData::XY => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(1.0, 0.0, 0.0), - y_axis: Point3d::new(0.0, 1.0, 0.0), - z_axis: Point3d::new(0.0, 0.0, 1.0), - value: PlaneType::XY, - meta: vec![], - }, - PlaneData::NegXY => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(1.0, 0.0, 0.0), - y_axis: Point3d::new(0.0, 1.0, 0.0), - z_axis: Point3d::new(0.0, 0.0, -1.0), - value: PlaneType::XY, - meta: vec![], - }, - PlaneData::XZ => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(1.0, 0.0, 0.0), - y_axis: Point3d::new(0.0, 0.0, 1.0), - z_axis: Point3d::new(0.0, -1.0, 0.0), - value: PlaneType::XZ, - meta: vec![], - }, - PlaneData::NegXZ => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(-1.0, 0.0, 0.0), - y_axis: Point3d::new(0.0, 0.0, 1.0), - z_axis: Point3d::new(0.0, 1.0, 0.0), - value: PlaneType::XZ, - meta: vec![], - }, - PlaneData::YZ => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(0.0, 1.0, 0.0), - y_axis: Point3d::new(0.0, 0.0, 1.0), - z_axis: Point3d::new(1.0, 0.0, 0.0), - value: PlaneType::YZ, - meta: vec![], - }, - PlaneData::NegYZ => Plane { - id, - origin: Point3d::new(0.0, 0.0, 0.0), - x_axis: Point3d::new(0.0, 1.0, 0.0), - y_axis: Point3d::new(0.0, 0.0, 1.0), - z_axis: Point3d::new(-1.0, 0.0, 0.0), - value: PlaneType::YZ, - meta: vec![], - }, - PlaneData::Plane { - origin, - x_axis, - y_axis, - z_axis, - } => Plane { - id, - origin: *origin, - x_axis: *x_axis, - y_axis: *y_axis, - z_axis: *z_axis, - value: PlaneType::Custom, - meta: vec![], - }, - } - } -} - /// Start a sketch on a specific plane or face. pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result { let (data, tag): (SketchData, Option) = args.get_data_and_optional_tag()?; @@ -1089,7 +1044,7 @@ async fn inner_start_sketch_on( ) -> Result { match data { SketchData::Plane(plane_data) => { - let plane = start_sketch_on_plane(plane_data, args).await?; + let plane = start_sketch_on_plane(plane_data, exec_state, args).await?; Ok(SketchSurface::Plane(plane)) } SketchData::Solid(solid) => { @@ -1125,11 +1080,19 @@ async fn start_sketch_on_face( })) } -async fn start_sketch_on_plane(data: PlaneData, args: &Args) -> Result, KclError> { - let mut plane: Plane = data.clone().into(); +async fn start_sketch_on_plane( + data: PlaneData, + exec_state: &mut ExecState, + args: &Args, +) -> Result, KclError> { + let mut plane = Plane::from_plane_data(data.clone(), exec_state); // Get the default planes. - let default_planes = args.ctx.engine.default_planes(args.source_range).await?; + let default_planes = args + .ctx + .engine + .default_planes(&mut exec_state.id_generator, args.source_range) + .await?; plane.id = match data { PlaneData::XY => default_planes.xy, @@ -1145,7 +1108,7 @@ async fn start_sketch_on_plane(data: PlaneData, args: &Args) -> Result { // Create the custom plane on the fly. - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, ModelingCmd::from(mcmd::MakePlane { @@ -1228,7 +1191,7 @@ pub(crate) async fn inner_start_profile_at( // Enter sketch mode on the surface. // We call this here so you can reuse the sketch surface for multiple sketches. - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, ModelingCmd::from(mcmd::EnableSketchMode { @@ -1246,8 +1209,8 @@ pub(crate) async fn inner_start_profile_at( ) .await?; - let id = uuid::Uuid::new_v4(); - let path_id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); + let path_id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd(path_id, ModelingCmd::from(mcmd::StartPath {})) .await?; @@ -1375,10 +1338,10 @@ pub(crate) fn inner_profile_start(sketch: Sketch) -> Result<[f64; 2], KclError> } /// Close the current sketch. -pub async fn close(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn close(exec_state: &mut ExecState, args: Args) -> Result { let (sketch, tag): (Sketch, Option) = args.get_sketch_and_optional_tag()?; - let new_sketch = inner_close(sketch, tag, args).await?; + let new_sketch = inner_close(sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -1407,11 +1370,16 @@ pub async fn close(_exec_state: &mut ExecState, args: Args) -> Result, args: Args) -> Result { +pub(crate) async fn inner_close( + sketch: Sketch, + tag: Option, + exec_state: &mut ExecState, + args: Args, +) -> Result { let from = sketch.current_pen_position()?; let to: Point2d = sketch.start.from.into(); - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id })) .await?; @@ -1420,7 +1388,7 @@ pub(crate) async fn inner_close(sketch: Sketch, tag: Option, args if let SketchSurface::Plane(_) = sketch.on { // We were on a plane, disable the sketch mode. args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable {}), ) .await?; @@ -1478,10 +1446,10 @@ pub enum ArcData { } /// Draw an arc. -pub async fn arc(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (ArcData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_arc(data, sketch, tag, args).await?; + let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -1514,6 +1482,7 @@ pub(crate) async fn inner_arc( data: ArcData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from: Point2d = sketch.current_pen_position()?; @@ -1542,7 +1511,7 @@ pub(crate) async fn inner_arc( })); } - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, @@ -1596,10 +1565,10 @@ pub enum TangentialArcData { } /// Draw a tangential arc. -pub async fn tangential_arc(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (TangentialArcData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_tangential_arc(data, sketch, tag, args).await?; + let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -1633,6 +1602,7 @@ async fn inner_tangential_arc( data: TangentialArcData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from: Point2d = sketch.current_pen_position()?; @@ -1644,7 +1614,7 @@ async fn inner_tangential_arc( tangent_info.center_or_tangent_point }; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); let (center, to, ccw) = match data { TangentialArcData::RadiusAndOffset { radius, offset } => { @@ -1723,18 +1693,18 @@ fn tan_arc_to(sketch: &Sketch, to: &[f64; 2]) -> ModelingCmd { } /// Draw a tangential arc to a specific point. -pub async fn tangential_arc_to(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result { let (to, sketch, tag): ([f64; 2], Sketch, Option) = super::args::FromArgs::from_args(&args, 0)?; - let new_sketch = inner_tangential_arc_to(to, sketch, tag, args).await?; + let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } /// Draw a tangential arc to point some distance away.. -pub async fn tangential_arc_to_relative(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn tangential_arc_to_relative(exec_state: &mut ExecState, args: Args) -> Result { let (delta, sketch, tag): ([f64; 2], Sketch, Option) = super::args::FromArgs::from_args(&args, 0)?; - let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, args).await?; + let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -1762,6 +1732,7 @@ async fn inner_tangential_arc_to( to: [f64; 2], sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from: Point2d = sketch.current_pen_position()?; @@ -1780,7 +1751,7 @@ async fn inner_tangential_arc_to( }); let delta = [to_x - from.x, to_y - from.y]; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?; let current_path = Path::TangentialArcTo { @@ -1831,6 +1802,7 @@ async fn inner_tangential_arc_to_relative( delta: [f64; 2], sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from: Point2d = sketch.current_pen_position()?; @@ -1864,7 +1836,7 @@ async fn inner_tangential_arc_to_relative( })); } - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?; let current_path = Path::TangentialArcTo { @@ -1905,10 +1877,10 @@ pub struct BezierData { } /// Draw a bezier curve. -pub async fn bezier_curve(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result { let (data, sketch, tag): (BezierData, Sketch, Option) = args.get_data_and_sketch_and_tag()?; - let new_sketch = inner_bezier_curve(data, sketch, tag, args).await?; + let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -1937,6 +1909,7 @@ async fn inner_bezier_curve( data: BezierData, sketch: Sketch, tag: Option, + exec_state: &mut ExecState, args: Args, ) -> Result { let from = sketch.current_pen_position()?; @@ -1945,7 +1918,7 @@ async fn inner_bezier_curve( let delta = data.to; let to = [from.x + data.to[0], from.y + data.to[1]]; - let id = uuid::Uuid::new_v4(); + let id = exec_state.id_generator.next_uuid(); args.batch_modeling_cmd( id, @@ -1984,10 +1957,10 @@ async fn inner_bezier_curve( } /// Use a sketch to cut a hole in another sketch. -pub async fn hole(_exec_state: &mut ExecState, args: Args) -> Result { +pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result { let (hole_sketch, sketch): (SketchSet, Sketch) = args.get_sketches()?; - let new_sketch = inner_hole(hole_sketch, sketch, args).await?; + let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?; Ok(KclValue::new_user_val(new_sketch.meta.clone(), new_sketch)) } @@ -2025,11 +1998,16 @@ pub async fn hole(_exec_state: &mut ExecState, args: Args) -> Result Result { +async fn inner_hole( + hole_sketch: SketchSet, + sketch: Sketch, + exec_state: &mut ExecState, + args: Args, +) -> Result { let hole_sketches: Vec = hole_sketch.into(); for hole_sketch in hole_sketches { args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::Solid2dAddHole { object_id: sketch.id, hole_id: hole_sketch.id, @@ -2040,7 +2018,7 @@ async fn inner_hole(hole_sketch: SketchSet, sketch: Sketch, args: Args) -> Resul // suggestion (mike) // we also hide the source hole since its essentially "consumed" by this operation args.batch_modeling_cmd( - uuid::Uuid::new_v4(), + exec_state.id_generator.next_uuid(), ModelingCmd::from(mcmd::ObjectVisible { object_id: hole_sketch.id, hidden: true, diff --git a/src/wasm-lib/kcl/src/test_server.rs b/src/wasm-lib/kcl/src/test_server.rs index 5f5a51b96..927eddc4e 100644 --- a/src/wasm-lib/kcl/src/test_server.rs +++ b/src/wasm-lib/kcl/src/test_server.rs @@ -1,7 +1,7 @@ //! Types used to send data to the test server. use crate::{ - executor::{ExecutorContext, ExecutorSettings}, + executor::{ExecutorContext, ExecutorSettings, IdGenerator}, settings::types::UnitLength, }; @@ -29,7 +29,9 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R let parser = crate::parser::Parser::new(tokens); let program = parser.ast()?; - let snapshot = ctx.execute_and_prepare_snapshot(&program).await?; + let snapshot = ctx + .execute_and_prepare_snapshot(&program, IdGenerator::default()) + .await?; // Create a temporary file to write the output to. let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4())); diff --git a/src/wasm-lib/output.txt b/src/wasm-lib/output.txt deleted file mode 100644 index 44a1780e6..000000000 --- a/src/wasm-lib/output.txt +++ /dev/null @@ -1,142 +0,0 @@ - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 0.00s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 824 filtered out; finished in 0.00s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - -running 1 test -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -test visuals::server_rack_heavy has been running for over 60 seconds -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -extrude -test visuals::server_rack_heavy ... FAILED - -failures: - -failures: - visuals::server_rack_heavy - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 142 filtered out; finished in 279.58s - diff --git a/src/wasm-lib/src/wasm.rs b/src/wasm-lib/src/wasm.rs index 03c8eda73..b0d807b89 100644 --- a/src/wasm-lib/src/wasm.rs +++ b/src/wasm-lib/src/wasm.rs @@ -16,6 +16,7 @@ use wasm_bindgen::prelude::*; pub async fn execute_wasm( program_str: &str, memory_str: &str, + id_generator_str: &str, units: &str, engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager, fs_manager: kcl_lib::fs::wasm::FileSystemManager, @@ -26,6 +27,8 @@ pub async fn execute_wasm( let program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?; let memory: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?; + let id_generator: kcl_lib::executor::IdGenerator = + serde_json::from_str(id_generator_str).map_err(|e| e.to_string())?; let units = kcl_lib::settings::types::UnitLength::from_str(units).map_err(|e| e.to_string())?; let engine: std::sync::Arc> = if is_mock { @@ -58,13 +61,16 @@ pub async fn execute_wasm( context_type, }; - let exec_state = ctx.run(&program, Some(memory)).await.map_err(String::from)?; + let exec_state = ctx + .run(&program, Some(memory), id_generator) + .await + .map_err(String::from)?; // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the // gloo-serialize crate instead. - // DO NOT USE serde_wasm_bindgen::to_value(&memory).map_err(|e| e.to_string()) + // DO NOT USE serde_wasm_bindgen::to_value(&exec_state).map_err(|e| e.to_string()) // it will break the frontend. - JsValue::from_serde(&exec_state.memory).map_err(|e| e.to_string()) + JsValue::from_serde(&exec_state).map_err(|e| e.to_string()) } // wasm_bindgen wrapper for execute @@ -93,7 +99,7 @@ pub async fn make_default_planes( .await .map_err(|e| format!("{:?}", e))?; let default_planes = engine - .new_default_planes(Default::default()) + .new_default_planes(&mut kcl_lib::executor::IdGenerator::default(), Default::default()) .await .map_err(String::from)?; diff --git a/src/wasm-lib/tests/executor/no_visuals.rs b/src/wasm-lib/tests/executor/no_visuals.rs index 87d66f70c..fa9406129 100644 --- a/src/wasm-lib/tests/executor/no_visuals.rs +++ b/src/wasm-lib/tests/executor/no_visuals.rs @@ -1,4 +1,8 @@ -use kcl_lib::{ast::types::Program, errors::KclError, executor::ExecutorContext}; +use kcl_lib::{ + ast::types::Program, + errors::KclError, + executor::{ExecutorContext, IdGenerator}, +}; macro_rules! gen_test { ($file:ident) => { @@ -22,12 +26,12 @@ macro_rules! gen_test_fail { } async fn run(code: &str) { - let (ctx, program) = setup(code).await; + let (ctx, program, id_generator) = setup(code).await; - ctx.run(&program, None).await.unwrap(); + ctx.run(&program, None, id_generator).await.unwrap(); } -async fn setup(program: &str) -> (ExecutorContext, Program) { +async fn setup(program: &str) -> (ExecutorContext, Program, IdGenerator) { let tokens = kcl_lib::token::lexer(program).unwrap(); let parser = kcl_lib::parser::Parser::new(tokens); let program = parser.ast().unwrap(); @@ -40,12 +44,12 @@ async fn setup(program: &str) -> (ExecutorContext, Program) { settings: Default::default(), context_type: kcl_lib::executor::ContextType::Mock, }; - (ctx, program) + (ctx, program, IdGenerator::default()) } async fn run_fail(code: &str) -> KclError { - let (ctx, program) = setup(code).await; - let Err(e) = ctx.run(&program, None).await else { + let (ctx, program, id_generator) = setup(code).await; + let Err(e) = ctx.run(&program, None, id_generator).await else { panic!("Expected this KCL program to fail, but it (incorrectly) never threw an error."); }; e diff --git a/src/wasm-lib/tests/modify/main.rs b/src/wasm-lib/tests/modify/main.rs index 77880d86f..862e4ebce 100644 --- a/src/wasm-lib/tests/modify/main.rs +++ b/src/wasm-lib/tests/modify/main.rs @@ -1,7 +1,7 @@ use anyhow::Result; use kcl_lib::{ ast::{modify::modify_ast_for_sketch, types::Program}, - executor::{ExecutorContext, KclValue, PlaneType, Sketch, SourceRange}, + executor::{ExecutorContext, IdGenerator, KclValue, PlaneType, Sketch, SourceRange}, }; use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, shared::Point3d, ModelingCmd}; use pretty_assertions::assert_eq; @@ -35,7 +35,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid let parser = kcl_lib::parser::Parser::new(tokens); let program = parser.ast()?; let ctx = kcl_lib::executor::ExecutorContext::new(&client, Default::default()).await?; - let exec_state = ctx.run(&program, None).await?; + let exec_state = ctx.run(&program, None, IdGenerator::default()).await?; // We need to get the sketch ID. // Get the sketch ID from memory.