Compare commits

...

24 Commits

Author SHA1 Message Date
86ba586318 Release new kcl-lib and derive-docs (#5344) 2025-02-11 14:56:23 -05:00
cc313afb89 Remove DEV=false from .env.production (#5342)
* Remove DEV=false from .env.production

* And process.env.DEV too

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Bad bot

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 13:19:23 -05:00
d0c8311e41 Fix deep links on Linux .desktop files (#5337)
* Fix deep links on Linux .desktop files
Fixes #5325

* Fix lint
2025-02-11 17:25:46 +00:00
28311d160a Fix: stable E2E tests on ubuntu localhost (#5269)
* fix: Needed to increase timeout for this test. waitForExecutionDone does not work since there are errors in the KCL code

* fix: again another wait for execution does not work

* fix: bug that desyncs codeManager, executeCode, lspPlugin

* fix: fixing react race condition on parsing numeric literals in command bar on open

* fix: adding comment to clarify the gotcha

* fix: saving off debugging...

* fix: added wait for execution done

* fix: removing testing code

* fix: adding wait for execution done

* fix: adding execution done wait

* fix: only fixes the chamfer point and click delete

* fix: add 1250ms before every progresscmdbar, removed model loaded scene state that is flaky, added toast if someone presses the command bar too quickly

* fix: adding a wait for execution

* fix: updating wait for execution

* fix: wait for execution done

* fix: wait for execution done

* fix: not waiting for scene, not waiting for command bar

* fix: restoring name

* fix: auto fixes

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: bad prompt fix

* fix: Fixed testing selections with wait

* fix: last wait fix

* fix: trying to resolve more flakes when running with more workers

* chore: adding a skipLocalEngine tag

* fix: fixing test when using local engine

* fix: codespell

* fix: big if true

* chore: skipping one more local engine tests that fails

* fix: auto fix:

* fix: restoring this testing code

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 12:13:25 -05:00
162856064b Dedicated installation path for nightly builds on Windows (#5331)
* WIP: Fix deep links on Windows for the nightly app
Fixes #5326

* Add nsis.include flip

* Clean up for PR
2025-02-11 11:18:33 -05:00
max
652f82e8c3 Fix Shift-Click to Deselect Edges/Faces (#5289)
* fix selections and support deselections

* test deselections

* rever commit

* undo sloppy merge

* re-commit the test

* make ubuntu happy

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* support initial sketch

* test sketch

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 08:16:48 -05:00
d4d9bf6c7f Add trivia to AST Node (#5330)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 05:30:14 +00:00
0e9d37c0a0 Factor out a modules module and add main.rs (#5329)
* Factor out a modules module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Very simple cargo run

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 13:52:46 +13:00
9c18060d73 UoM types in AST, unnamed annotations, and syntax for importing from std (#5324)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-11 11:26:14 +13:00
e2be66b024 Fix: inserting revolve command accounts for axis dependency (#5231)
fix: inserting revolve with axis dependency
2025-02-10 14:01:06 -06:00
1bb372b642 Add helix to the artifact graph (#5317)
* Add helix simulation test

* Update output for new test

* Add helix to the artifact graph

* Update output since adding helix to the graph

* Fix helix selection-based deletion (#5323)

* Fix helix selection-based deletion

* Run e2e tests on all PRs

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-10 19:24:00 +00:00
627fbda671 Bump @types/node from 22.10.6 to 22.13.1 in /packages/codemirror-lsp-client (#5285) 2025-02-10 09:29:26 -05:00
74bdb72edc Bump serde_json from 1.0.137 to 1.0.138 in /src/wasm-lib in the serde-dependencies group (#5222) 2025-02-10 05:00:17 +00:00
a9182bf357 Bump typescript-eslint from 8.19.1 to 8.23.0 (#5322) 2025-02-10 04:28:21 +00:00
5f14023404 Split up creating a new simulation test and running it (#5316) 2025-02-08 00:28:39 -05:00
9a3ac64603 KCL: Appearance stdlib fn is now kwargs (#5308) 2025-02-07 16:36:51 -06:00
67e60cb832 fix: remove noisy log line in linux (#5306)
fix: remove noisy log line

Co-authored-by: Tom Pridham <pridham.tom@gmail.com>
2025-02-07 19:23:06 +00:00
110037df79 Fix: update sweep snapshots code after kwargs merge (#5307)
* Fix: update sweep snapshots code after kwargs merge

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 13:51:28 -05:00
30b1dae38a KCL: Sweep stdlib fn now uses keyword args (#5300)
Before:
```
|> sweep({ path = myPath }, %)
```

After:
```
|> sweep(path = myPath)
```
2025-02-07 18:35:04 +00:00
f20fc5b467 Remove units from share link flow, enable it on nightly (#5304)
* Remove units from the share link flow

They should rely on inline settings annotations

* @pierremtb's fix to turn on the menu item in nightly

* fmt

* Don't show web banner if the create file query param is present

* Change copy to 'Share current part (via Zoo link)'

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-02-07 18:19:56 +00:00
1bfc3a0a3c CM KCL: add named args to fn calls (#5303)
* CM KCL: add named args to fn calls

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Rename to match other code

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 16:47:45 +00:00
f6e975db84 Add a "back" button to the onboarding buttons, move the dismiss button to a little corner x button (#5296)
* Add previous button to OnboardingButtons, move dismiss to popover corner

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up diagnostics

I am thoroughly enjoying nvim now

* Amend "click through" test to also click back

* fmt

* Set this test back to fixme, that work should be its own PR

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 11:30:36 -05:00
b82eec85fd Add basic axis case for Helix point-and-click (#5240)
* Move Helix button to a section with offset plane (3d 'construction' elements)
Fixes #5234

* Add generix axis case for Helix point-and-click
Fixes #5072 #5236 #5073

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Clean up

* Temp remove point and click test

* Add back point and click test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Fix helix arg after change to kwargs

* More fixes wrt helix arg after change to kwargs

* Fixed thanks to @adamchalmers

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-07 10:16:36 -05:00
5dc4213295 let users have big editors (#5295)
* let users have big editor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Update src/components/ModelingSidebar/ModelingSidebar.tsx

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-02-07 05:19:09 -05:00
179 changed files with 12601 additions and 2994 deletions

View File

@ -1,5 +1,4 @@
NODE_ENV=production
DEV=false
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev

View File

@ -3,7 +3,6 @@ on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,8 @@ test.describe(
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// FIXME: Cannot use scene.waitForExecutionDone() since there is no KCL code
await page.waitForTimeout(10000)
await u.openDebugPanel()
const coord =

View File

@ -10,6 +10,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
test('Typing KCL errors induces a badge on the code pane button', async ({
page,
homePage,
scene,
}) => {
const u = await getUtils(page)
@ -30,11 +31,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
// Ensure no badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
@ -175,7 +172,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(1000)
// FIXME: await scene.waitForExecutionDone() does not work. It still fails.
// I needed to increase this timeout to get this to pass.
await page.waitForTimeout(10000)
// Ensure badge is present
const codePaneButtonHolder = page.locator('#code-button-holder')
@ -187,7 +186,7 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
// click in the editor to focus it
await page.locator('.cm-content').click()
await page.waitForTimeout(500)
await page.waitForTimeout(2000)
// go to the start of the editor and enter more text which will trigger
// a lint error.
@ -204,8 +203,9 @@ test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type('foo_bar = 1')
await page.waitForTimeout(500)
await page.waitForTimeout(2000)
await page.keyboard.press('Enter')
await page.waitForTimeout(2000)
// ensure we have a lint error
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()

View File

@ -174,6 +174,9 @@ test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// FIXME: No KCL code, unable to wait for engine execution
await page.waitForTimeout(10000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()

View File

@ -9,8 +9,8 @@ import fsp from 'fs/promises'
test(
'export works on the first try',
{ tag: '@electron' },
async ({ page, context }, testInfo) => {
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -118,8 +118,9 @@ test(
// Close the file pane
await u.closeFilePanel()
// wait for it to finish executing (todo: make this more robust)
await page.waitForTimeout(1000)
// FIXME: await scene.waitForExecutionDone() does not work. The modeling indicator stays in -receive-reliable and not execution done
await page.waitForTimeout(10000)
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()

View File

@ -490,6 +490,11 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
await page.keyboard.press('ArrowLeft')
await page.keyboard.press('ArrowRight')
// FIXME: lsp errors do not propagate to the frontend until engine is connected and code is executed
// This timeout is to wait for engine connection. LSP and code execution errors should be handled differently
// LSP can emit errors as fast as it waits and show them in the editor
await page.waitForTimeout(10000)
// error in guter
await expect(page.locator('.cm-lint-marker-info').first()).toBeVisible()
@ -641,7 +646,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
width = 0.500
height = 0.500
dia = 4
fn squareHole = (l, w) => {
squareHoleSketch = startSketchOn('XY')
|> startProfileAt([-width / 2, -length / 2], %)
@ -714,7 +719,7 @@ test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|> line(end = [0, -10], tag = $revolveAxis)
|> close()
|> extrude(length = 10)
sketch001 = startSketchOn(box, revolveAxis)
|> startProfileAt([5, 10], %)
|> line(end = [0, -10])

View File

@ -112,6 +112,9 @@ export class CmdBarFixture {
* and assumes we are past the `pickCommand` step.
*/
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
// FIXME: Progressing the command bar is a race condition. We have an async useEffect that reports the final state via useCalculateKclExpression. If this does not run quickly enough, it will not "fail" the continue because you can press continue if the state is not ready. E2E tests do not know this.
// Wait 1250ms to assume the await executeAst of the KCL input field is finished
await this.page.waitForTimeout(1250)
if (shouldFuzzProgressMethod || Math.random() > 0.5) {
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
@ -128,6 +131,23 @@ export class CmdBarFixture {
}
}
// Added data-testid to the command bar buttons
// command-bar-continue are the buttons to go to the next step
// does not include the submit which is the final button press
// aka the right arrow button
continue = async () => {
const continueButton = this.page.getByTestId('command-bar-continue')
await continueButton.click()
}
// Added data-testid to the command bar buttons
// command-bar-submit is the button for the final step to submit
// the command bar flow aka the checkmark button.
submit = async () => {
const submitButton = this.page.getByTestId('command-bar-submit')
await submitButton.click()
}
openCmdBar = async (selectCmd?: 'promptToEdit') => {
// TODO why does this button not work in electron tests?
// await this.cmdBarOpenBtn.click()

View File

@ -20,6 +20,7 @@ export class ToolbarFixture {
shellButton!: Locator
revolveButton!: Locator
offsetPlaneButton!: Locator
helixButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
rectangleBtn!: Locator
@ -49,6 +50,7 @@ export class ToolbarFixture {
this.shellButton = page.getByTestId('shell')
this.revolveButton = page.getByTestId('revolve')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.helixButton = page.getByTestId('helix')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.rectangleBtn = page.getByTestId('corner-rectangle')

View File

@ -27,7 +27,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
@ -68,7 +68,7 @@ test.describe('Onboarding tests', () => {
},
cleanProjectDir: true,
},
async ({ page, homePage }, testInfo) => {
async ({ page }) => {
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -154,7 +154,7 @@ test.describe('Onboarding tests', () => {
)
test(
'Click through each onboarding step',
'Click through each onboarding step and back',
{
appSettings: {
app: {
@ -187,15 +187,21 @@ test.describe('Onboarding tests', () => {
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
// Finish the onboarding
await nextButton.hover()
await nextButton.click()
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
@ -269,7 +275,7 @@ test.describe('Onboarding tests', () => {
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
@ -336,10 +342,10 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene()
// Test that the text in this step is correct
const avatarLocator = await page
const avatarLocator = page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
@ -447,7 +453,7 @@ test.fixme(
},
cleanProjectDir: true,
},
async ({ context, page, homePage }, testInfo) => {
async ({ context, page }) => {
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true })
@ -486,10 +492,6 @@ test.fixme(
})
await test.step('Navigate into project', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log)
await expect(
page.getByRole('heading', { name: 'Your Projects' })
).toBeVisible()

View File

@ -29,11 +29,13 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
await scene.clickNoWhere()
// FIXME: Do not click, clicking removes the activeLines in future checks
// await scene.clickNoWhere()
await expect(toolbar.extrudeButton).toBeEnabled()
})
@ -199,6 +201,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -422,6 +425,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -712,6 +716,330 @@ openSketch = startSketchOn('XY')
})
})
test(`Shift-click to select and deselect edges and faces`, async ({
context,
page,
homePage,
scene,
}) => {
// Code samples
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-12, -6], %)
|> line(end = [0, 12])
|> line(end = [24, 0])
|> line(end = [0, -12])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(%, length = -12)`
// Locators
const upperEdgeLocation = { x: 600, y: 192 }
const lowerEdgeLocation = { x: 600, y: 383 }
const faceLocation = { x: 630, y: 290 }
// Click helpers
const [clickOnUpperEdge] = scene.makeMouseHelpers(
upperEdgeLocation.x,
upperEdgeLocation.y
)
const [clickOnLowerEdge] = scene.makeMouseHelpers(
lowerEdgeLocation.x,
lowerEdgeLocation.y
)
const [clickOnFace] = scene.makeMouseHelpers(faceLocation.x, faceLocation.y)
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220] // varies from 192 to 255
const edgeColorYellow: [number, number, number] = [251, 251, 40] // vaies from 12 to 67
const faceColorGray: [number, number, number] = [168, 168, 168]
const faceColorYellow: [number, number, number] = [155, 155, 155]
const tolerance = 40
const timeout = 150
// Setup
await test.step(`Initial test setup`, async () => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Wait for the scene and stream to load
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
})
await test.step('Select and deselect a single edge', async () => {
await test.step('Click the edge', async () => {
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
await clickOnUpperEdge()
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
})
await test.step('Shift-click the same edge to deselect', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnUpperEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
})
})
await test.step('Select and deselect multiple objects', async () => {
await test.step('Select both edges and the face', async () => {
await test.step('Select the upper edge', async () => {
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
await clickOnUpperEdge()
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
})
await test.step('Select the lower edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorWhite,
lowerEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnLowerEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorYellow,
lowerEdgeLocation,
tolerance
)
})
await test.step('Select the face (Shift-click)', async () => {
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnFace()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
})
})
await test.step('Deselect them one by one', async () => {
await test.step('Deselect the face (Shift-click)', async () => {
await scene.expectPixelColor(faceColorYellow, faceLocation, tolerance)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnFace()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(faceColorGray, faceLocation, tolerance)
})
await test.step('Deselect the lower edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorYellow,
lowerEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnLowerEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
lowerEdgeLocation,
tolerance
)
})
await test.step('Deselect the upper edge (Shift-click)', async () => {
await scene.expectPixelColor(
edgeColorYellow,
upperEdgeLocation,
tolerance
)
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickOnUpperEdge()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
upperEdgeLocation,
tolerance
)
})
})
})
})
test(`Shift-click to select and deselect sketch segments`, async ({
page,
homePage,
scene,
editor,
}) => {
// Locators
const firstPointLocation = { x: 200, y: 100 }
const secondPointLocation = { x: 800, y: 100 }
const thirdPointLocation = { x: 800, y: 400 }
const fristSegmentLocation = { x: 750, y: 100 }
const secondSegmentLocation = { x: 800, y: 150 }
const planeLocation = { x: 700, y: 200 }
// Click helpers
const [clickFirstPoint] = scene.makeMouseHelpers(
firstPointLocation.x,
firstPointLocation.y
)
const [clickSecondPoint] = scene.makeMouseHelpers(
secondPointLocation.x,
secondPointLocation.y
)
const [clickThirdPoint] = scene.makeMouseHelpers(
thirdPointLocation.x,
thirdPointLocation.y
)
const [clickFirstSegment] = scene.makeMouseHelpers(
fristSegmentLocation.x,
fristSegmentLocation.y
)
const [clickSecondSegment] = scene.makeMouseHelpers(
secondSegmentLocation.x,
secondSegmentLocation.y
)
const [clickPlane] = scene.makeMouseHelpers(
planeLocation.x,
planeLocation.y
)
// Colors
const edgeColorWhite: [number, number, number] = [220, 220, 220]
const edgeColorBlue: [number, number, number] = [20, 20, 200]
const backgroundColor: [number, number, number] = [30, 30, 30]
const tolerance = 40
const timeout = 150
// Setup
await test.step(`Initial test setup`, async () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// Wait for the scene and stream to load
await scene.expectPixelColor(
backgroundColor,
secondPointLocation,
tolerance
)
})
await test.step('Select and deselect a single sketch segment', async () => {
await test.step('Get into sketch mode', async () => {
await editor.closePane()
await page.waitForTimeout(timeout)
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(timeout)
await clickPlane()
await page.waitForTimeout(1000)
})
await test.step('Draw sketch', async () => {
await clickFirstPoint()
await page.waitForTimeout(timeout)
await clickSecondPoint()
await page.waitForTimeout(timeout)
await clickThirdPoint()
await page.waitForTimeout(timeout)
})
await test.step('Deselect line tool', async () => {
const btnLine = page.getByTestId('line')
const btnLineAriaPressed = await btnLine.getAttribute('aria-pressed')
if (btnLineAriaPressed === 'true') {
await btnLine.click()
}
await page.waitForTimeout(timeout)
})
await test.step('Select the first segment', async () => {
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
await test.step('Select the second segment (Shift-click)', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorBlue,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the first segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickFirstSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorBlue,
secondSegmentLocation,
tolerance
)
})
await test.step('Deselect the second segment', async () => {
await page.keyboard.down('Shift')
await page.waitForTimeout(timeout)
await clickSecondSegment()
await page.waitForTimeout(timeout)
await page.keyboard.up('Shift')
await scene.expectPixelColor(
edgeColorWhite,
fristSegmentLocation,
tolerance
)
await scene.expectPixelColor(
edgeColorWhite,
secondSegmentLocation,
tolerance
)
})
})
})
test(`Offset plane point-and-click`, async ({
context,
page,
@ -727,6 +1055,9 @@ openSketch = startSketchOn('XY')
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
// The engine may not be connected
await page.waitForTimeout(15000)
await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
@ -775,6 +1106,71 @@ openSketch = startSketchOn('XY')
})
})
test('Helix point-and-click', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
// One dumb hardcoded screen pixel value
const testPoint = { x: 620, y: 257 }
const expectedOutput = `helix001 = helix(revolutions = 1, angleStart = 360, counterClockWise = false, radius = 5, axis = 'X', length = 5)`
await homePage.goToModelingScene()
await test.step(`Look for the red of the default plane`, async () => {
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.helixButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'revolutions',
currentArgValue: '1',
headerArguments: {
AngleStart: '',
Axis: '',
CounterClockWise: '',
Length: '',
Radius: '',
Revolutions: '',
},
highlightedHeaderArg: 'revolutions',
commandName: 'Helix',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedOutput],
highlightedCode: '',
})
// Red plane is now gone, white helix is there
await scene.expectPixelColor([250, 250, 250], testPoint, 15)
})
await test.step('Delete offset plane via feature tree selection', async () => {
await editor.closePane()
const operationButton = await toolbar.getFeatureTreeOperation('Helix', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
// Red plane is back
await scene.expectPixelColor([96, 52, 52], testPoint, 15)
})
})
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
@ -887,6 +1283,7 @@ loft001 = loft([sketch001, sketch002])
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
@ -964,7 +1361,7 @@ sketch002 = startSketchOn('XZ')
testPoint.x - 50,
testPoint.y
)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
const sweepDeclaration = 'sweep001 = sweep(sketch001, path = sketch002)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
@ -1529,16 +1926,7 @@ extrude001 = extrude(sketch001, length = -12)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
// verify modeling scene is loaded
await scene.expectPixelColor(
backgroundColor,
secondEdgeLocation,
lowTolerance
)
// wait for stream to load
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
await scene.waitForExecutionDone()
})
// Test 1: Command bar flow with preselected edges
@ -1763,6 +2151,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// verify modeling scene is loaded
await scene.expectPixelColor(
@ -1885,6 +2274,7 @@ chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
@ -1983,6 +2373,7 @@ extrude001 = extrude(sketch001, length = 40)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 }
@ -2156,7 +2547,7 @@ extrude002 = extrude(sketch002, length = 50)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-2000, %)
sweep001 = sweep({ path = sketch002 }, sketch001)
sweep001 = sweep(sketch001, path = sketch002)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)

View File

@ -455,7 +455,7 @@ test.describe('Can export from electron app', () => {
for (const method of exportMethods) {
test(
`Can export using ${method}`,
{ tag: '@electron' },
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => {
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')

View File

@ -36,7 +36,7 @@ extrude003 = extrude(sketch003, length = 20)
`
test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
test.describe('Check the happy path, for basic changing color', () => {
test.fixme('Check the happy path, for basic changing color', () => {
const cases = [
{
desc: 'User accepts change',
@ -60,6 +60,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const greenCheckCoords = { x: 565, y: 345 }
@ -76,7 +77,9 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
'Submitting to Text-to-CAD API...'
)
const successToast = page.getByText('Prompt to edit successful')
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
const acceptBtn = page.getByRole('button', {
name: 'checkmark Accept',
})
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
await test.step('wait for scene to load select body and check selection came through', async () => {
@ -99,14 +102,16 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(submittingToast).toBeVisible()
await expect(submittingToast).not.toBeVisible({ timeout: 2 * 60_000 }) // can take a while
await expect(submittingToast).not.toBeVisible({
timeout: 2 * 60_000,
}) // can take a while
await expect(successToast).toBeVisible()
})
await test.step('verify initial change', async () => {
await scene.expectPixelColor(green, greenCheckCoords, 15)
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
await editor.expectEditor.toContain('appearance({')
await editor.expectEditor.toContain('appearance(')
})
if (!shouldReject) {
@ -115,13 +120,13 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(green, greenCheckCoords, 15)
await editor.expectEditor.toContain('appearance({')
await editor.expectEditor.toContain('appearance(')
// ctrl-z works after accepting
await page.keyboard.down('ControlOrMeta')
await page.keyboard.press('KeyZ')
await page.keyboard.up('ControlOrMeta')
await editor.expectEditor.not.toContain('appearance({')
await editor.expectEditor.not.toContain('appearance(')
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
})
} else {
@ -130,7 +135,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
await expect(successToast).not.toBeVisible()
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
await editor.expectEditor.not.toContain('appearance({')
await editor.expectEditor.not.toContain('appearance(')
})
}
})
@ -150,6 +155,7 @@ test.describe('Prompt-to-edit tests', { tag: '@skipWin' }, () => {
localStorage.setItem('persistCode', file)
}, file)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const body1CapCoords = { x: 571, y: 351 }
const [clickBody1Cap] = scene.makeMouseHelpers(

View File

@ -251,7 +251,7 @@ extrude001 = extrude(sketch001, length = 50)
|> yLineTo(0, %)
|> close()
|>
example = extrude(exampleSketch, length = 5)
shell(exampleSketch, faces = ['end'], thickness = 0.25)`
)
@ -306,113 +306,113 @@ extrude001 = extrude(sketch001, length = 50)
|> angledLine({ angle: 50, length: 45 }, %)
|> yLineTo(0, %)
|> close()
thing: "blah"`)
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
}
)
test('when engine fails export we handle the failure and alert the user', async ({
scene,
page,
homePage,
}) => {
const u = await getUtils(page)
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
)
test(
'when engine fails export we handle the failure and alert the user',
{ tag: '@skipLocalEngine' },
async ({ scene, page, homePage }) => {
const u = await getUtils(page)
await page.addInitScript(
async ({ code }) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
},
{ code: TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR }
)
await page.setBodyDimensions({ width: 1000, height: 500 })
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await homePage.goToModelingScene()
await u.waitForPageLoad()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Click the export button
await exportButton.click()
// Click the export button
await exportButton.click()
// Click the stl.
const stlOption = page.getByText('glTF')
await expect(stlOption).toBeVisible()
// Click the stl.
const stlOption = page.getByText('glTF')
await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
// Find the toast.
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(engineErrorToastMessage).toBeVisible()
const engineErrorToastMessage = page.getByText(`Nothing to export`)
await expect(engineErrorToastMessage).toBeVisible()
// Make sure the exporting toast is gone
await expect(exportingToastMessage).not.toBeVisible()
// Make sure the exporting toast is gone
await expect(exportingToastMessage).not.toBeVisible()
// Click the code editor
await page.locator('.cm-content').click()
// Click the code editor
await page.locator('.cm-content').click()
await page.waitForTimeout(2000)
await page.waitForTimeout(2000)
// Expect the toast to be gone
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
// Expect the toast to be gone
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
// Now add in code that works.
await page.locator('.cm-content').fill(bracket)
await page.keyboard.press('End')
await page.keyboard.press('Enter')
// Now add in code that works.
await page.locator('.cm-content').fill(bracket)
await page.keyboard.press('End')
await page.keyboard.press('Enter')
await scene.waitForExecutionDone()
await scene.waitForExecutionDone()
// Now try exporting
// Now try exporting
// Click the export button
await exportButton.click()
// Click the export button
await exportButton.click()
// Click the stl.
await expect(stlOption).toBeVisible()
// Click the stl.
await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
// Expect it to succeed.
await expect(exportingToastMessage).not.toBeVisible({ timeout: 15_000 })
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
})
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
}
)
test(
'ensure you can not export while an export is already going',
{ tag: ['@skipLinux', '@skipWin'] },

View File

@ -9,6 +9,7 @@ import {
PERSIST_MODELING_CONTEXT,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
import { SceneFixture } from './fixtures/sceneFixture'
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
@ -184,7 +185,8 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
const doEditSegmentsByDraggingHandle = async (
page: Page,
homePage: HomePageFixture,
openPanes: string[]
openPanes: string[],
scene: SceneFixture
) => {
// Load the app with the code panes
await page.addInitScript(async () => {
@ -200,6 +202,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
const u = await getUtils(page)
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -317,7 +320,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test(
'code pane open at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
async ({ page, homePage, scene }) => {
// Load the app with the code panes
await page.addInitScript(async () => {
localStorage.setItem(
@ -330,14 +333,14 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
})
)
})
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
await doEditSegmentsByDraggingHandle(page, homePage, ['code'], scene)
}
)
test(
'code pane closed at start-handles',
{ tag: ['@skipWin'] },
async ({ page, homePage }) => {
async ({ page, homePage, scene }) => {
// Load the app with the code panes
await page.addInitScript(async (persistModelingContext) => {
localStorage.setItem(
@ -345,7 +348,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
JSON.stringify({ openPanes: [] })
)
}, PERSIST_MODELING_CONTEXT)
await doEditSegmentsByDraggingHandle(page, homePage, [])
await doEditSegmentsByDraggingHandle(page, homePage, [], scene)
}
)
})
@ -547,6 +550,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
test('Can edit a sketch that has been revolved in the same pipe', async ({
page,
homePage,
scene,
}) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -562,6 +566,7 @@ test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })

View File

@ -1187,14 +1187,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
|> sweep(path = sweepPath)
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})
@ -1235,14 +1233,12 @@ sweepSketch = startSketchOn('XY')
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
|> sweep(path = sweepPath)
|> appearance(
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
)
`
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -3,199 +3,200 @@ import { test, expect } from './zoo-test'
import { commonPoints, getUtils } from './test-utils'
test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({
page,
homePage,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
test(
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await homePage.goToModelingScene()
const networkToggle = page.getByTestId('network-toggle')
const networkToggle = page.getByTestId('network-toggle')
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// This is how we wait until the stream is online
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkWidget = page.locator('[data-testid="network-toggle"]')
await expect(networkWidget).toBeVisible()
await networkWidget.hover()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
const networkPopover = page.locator('[data-testid="network-popover"]')
await expect(networkPopover).not.toBeVisible()
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// (First check) Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(100, 100)
await expect(networkPopover).not.toBeVisible()
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn off the network
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Click the network widget
await networkWidget.click()
// Click the network widget
await networkWidget.click()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Check the modal opened.
await expect(networkPopover).toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Click off the modal.
await page.mouse.click(0, 0)
await expect(networkPopover).not.toBeVisible()
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Turn back on the network
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
})
// (Second check) expect the network to be up
await expect(networkToggle).toContainText('Connected')
}
)
test('Engine disconnect & reconnect in sketch mode', async ({
page,
homePage,
}) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle')
test(
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio
await homePage.goToModelingScene()
await u.waitForPageLoad()
await homePage.goToModelingScene()
await u.waitForPageLoad()
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
await u.openDebugPanel()
// click on "Start Sketch" button
await u.clearCommandLogs()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(100)
// select a plane
await page.mouse.click(700, 200)
// select a plane
await page.mouse.click(700, 200)
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')`
)
await u.closeDebugPanel()
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')`
)
await u.closeDebugPanel()
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
await expect(page.locator('.cm-content'))
.toHaveText(`sketch001 = startSketchOn('XZ')
|> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`)
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network down
await u.emulateNetworkConditions({
offline: true,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Expect the network to be down
await expect(networkToggle).toContainText('Problem')
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// Ensure we are not in sketch mode
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// simulate network up
await u.emulateNetworkConditions({
offline: false,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Wait for the app to be ready for use
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled({ timeout: 15000 })
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Expect the network to be up
await expect(networkToggle).toContainText('Connected')
await expect(page.getByTestId('loading-stream')).not.toBeAttached()
// Click off the code pane.
await page.mouse.click(100, 100)
// Click off the code pane.
await page.mouse.click(100, 100)
// select a line
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
// select a line
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
// enter sketch again
await u.doAndWaitForCmd(
() => page.getByRole('button', { name: 'Edit Sketch' }).click(),
'default_camera_get_settings'
)
await page.waitForTimeout(150)
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
// Click the line tool
await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(150)
await page.waitForTimeout(150)
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ')
// Ensure we can continue sketching
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %)
|> xLine(12.34, %)
|> line(end = [-12.34, 12.34])
`)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await page.waitForTimeout(100)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ')
await expect.poll(u.normalisedEditorCode)
.toBe(`sketch001 = startSketchOn('XZ')
|> startProfileAt([12.34, -12.34], %)
|> xLine(12.34, %)
|> line(end = [-12.34, 12.34])
@ -203,20 +204,21 @@ test.describe('Test network and connection issues', () => {
`)
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Unequip line tool
await page.keyboard.press('Escape')
// Make sure we didn't pop out of sketch mode.
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(
page.getByRole('button', { name: 'line Line', exact: true })
).not.toHaveAttribute('aria-pressed', 'true')
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
})
// Exit sketch
await page.keyboard.press('Escape')
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible()
}
)
})

View File

@ -109,7 +109,8 @@ test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
await page.keyboard.down('Shift')
await page.mouse.move(600, 200)
await page.mouse.down({ button: 'right' })
await page.mouse.move(700, 200, { steps: 2 })
// Gotcha: remove steps:2 from this 700,200 mouse move. This bricked the test on local host engine.
await page.mouse.move(700, 200)
await page.mouse.up({ button: 'right' })
await page.keyboard.up('Shift')
}, [-19, -85, -85])

View File

@ -248,7 +248,11 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
})
})
test('Solids should be select and deletable', async ({ page, homePage }) => {
test('Solids should be select and deletable', async ({
page,
homePage,
scene,
}) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -320,10 +324,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -902,6 +903,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test('Testing selections (and hovers) work on sketches when NOT in sketch mode', async ({
page,
homePage,
scene,
}) => {
const cases = [
{
@ -937,6 +939,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -970,6 +973,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
test("Hovering and selection of extruded faces works, and is not overridden shortly after user's click", async ({
page,
homePage,
scene,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
@ -988,6 +992,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
@ -1021,19 +1026,19 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
.poll(() => u.getGreatestPixDiff(extrudeWall, noHoverColor))
.toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(100)
await page.waitForTimeout(1000)
await page.mouse.move(extrudeWall.x, extrudeWall.y)
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(extrudeText)
)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(
await u.getGreatestPixDiff(extrudeWall, hoverColor)
).toBeLessThan(15)
await page.mouse.click(extrudeWall.x, extrudeWall.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${extrudeText}`)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(
await u.getGreatestPixDiff(extrudeWall, selectColor)
).toBeLessThan(15)
@ -1044,7 +1049,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
).toBeLessThan(15)
await page.mouse.move(nothing.x, nothing.y)
await page.waitForTimeout(300)
await page.waitForTimeout(1000)
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
// because of shading, color is not exact everywhere on the face
@ -1058,11 +1063,11 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
await expect(page.getByTestId('hover-highlight').first()).toContainText(
removeAfterFirstParenthesis(capText)
)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(await u.getGreatestPixDiff(cap, hoverColor)).toBeLessThan(15)
await page.mouse.click(cap.x, cap.y)
await expect(page.locator('.cm-activeLine')).toHaveText(`|> ${capText}`)
await page.waitForTimeout(200)
await page.waitForTimeout(1000)
await expect(await u.getGreatestPixDiff(cap, selectColor)).toBeLessThan(15)
await page.waitForTimeout(1000)
// check color stays there, i.e. not overridden (this was a bug previously)
@ -1109,7 +1114,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [4.95, -8])
|> line(end = [-20.38, -10.12])
|> line(end = [-15.79, 17.08])
fn yohey = (pos) => {
sketch004 = startSketchOn('XZ')
${extrudeAndEditBlockedInFunction}
@ -1119,7 +1124,7 @@ test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|> line(end = [-15.79, 17.08])
return ''
}
yohey([15.79, -34.6])
`
)

View File

@ -32,15 +32,18 @@ test.fixme('Units menu', async ({ page, homePage }) => {
await expect(unitsMenuButton).toContainText('mm')
})
test('Successful export shows a success toast', async ({ page, homePage }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`topAng = 25
test(
'Successful export shows a success toast',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
// FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`topAng = 25
bottomAng = 35
baseLen = 3.5
baseHeight = 1
@ -78,26 +81,27 @@ part001 = startSketchOn('-XZ')
|> xLineTo(ZERO, %)
|> close()
|> extrude(length = 4)`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.waitForCmdReceive('extrude')
await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel()
await doExport(
{
type: 'gltf',
storage: 'embedded',
presentation: 'pretty',
},
page
)
})
}
)
test('Paste should not work unless an input is focused', async ({
page,
@ -444,7 +448,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings')
})
test('Sketch on face', async ({ page, homePage }) => {
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => {
test.setTimeout(90_000)
const u = await getUtils(page)
await page.addInitScript(async () => {
@ -470,11 +474,7 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await scene.waitForExecutionDone()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
@ -579,10 +579,9 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'arrow right Continue' }).click()
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await page.getByRole('button', { name: 'checkmark Submit command' }).click()
await cmdBar.progressCmdBar()
const result2 = result.genNext`
const sketch002 = extrude(sketch002, length = ${[5, 5]} + 7)`

View File

@ -32,10 +32,6 @@ win:
arch:
- x64
- arm64
# - target: msi
# arch:
# - x64
# - arm64
signingHashAlgorithms:
- sha256
sign: "./scripts/sign-win.js"
@ -47,15 +43,12 @@ win:
mimeType: text/vnd.zoo.kcl
description: Zoo KCL File
role: Editor
# msi:
# oneClick: false
# perMachine: true
nsis:
oneClick: false
perMachine: true
allowElevation: true
installerIcon: "assets/icon.ico"
include: "./installer.nsh"
include: "./scripts/installer.nsh"
linux:
artifactName: "${productName}-${version}-${arch}-${os}.${ext}"
target:

View File

@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-shell/manifest.json",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-appearance/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
@ -120,6 +120,7 @@
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
"test:playwright:electron:ubuntu:engine:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot|@skipLocalEngine'",
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
},
@ -200,7 +201,7 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.19.1",
"typescript-eslint": "^8.23.0",
"vite": "^5.4.12",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -59,7 +59,9 @@ UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName (":" | Equals) expression }
ArgumentList { "(" commaSep<expression> ")" }
LabeledArgument { ArgumentLabel Equals expression }
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
type[@isGroup=Type] {
@specialize[@name=PrimitiveType]<
@ -74,6 +76,8 @@ VariableDefinition { identifier }
VariableName { identifier }
ArgumentLabel { identifier }
@skip { whitespace | LineComment | BlockComment }
kw<term> { @specialize[@name={term}]<identifier, term> }

View File

@ -0,0 +1,85 @@
# empty
f()
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList)))
# single anon arg
f(1)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number))))
# deprecated multiple anon args
f(1, 2)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(Number,
Number))))
# deprecated trailing %
startSketchOn('XY')
|> line([thickness, 0], %)
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(ArrayExpression(VariableName,
Number),
PipeSubstitution)))))
# % and named arg
startSketchOn('XY')
|> line(%, end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(PipeSubstitution,
LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# implied % and named arg
startSketchOn('XY')
|> line(end = [thickness, 0])
==>
Program(ExpressionStatement(PipeExpression(CallExpression(VariableName,
ArgumentList(String)),
PipeOperator,
CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
ArrayExpression(VariableName,
Number)))))))
# multiple named arg
ngon(plane = "XY", numSides = 5, radius = pentR)
==>
Program(ExpressionStatement(CallExpression(VariableName,
ArgumentList(LabeledArgument(ArgumentLabel,
Equals,
String),
LabeledArgument(ArgumentLabel,
Equals,
Number),
LabeledArgument(ArgumentLabel,
Equals,
VariableName)))))

View File

@ -29,7 +29,7 @@
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@types/node": "^22.10.6",
"@types/node": "^22.13.1",
"ts-node": "^10.9.2"
}
}

View File

@ -109,10 +109,10 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/node@^22.10.6":
version "22.10.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239"
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==
"@types/node@^22.13.1":
version "22.13.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
dependencies:
undici-types "~6.20.0"

View File

@ -11,6 +11,7 @@ echo "$PACKAGE" > package.json
# electron-builder.yml
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
# Release notes
echo "Nightly build $VERSION (commit $COMMIT)" > release-notes.md

View File

@ -0,0 +1,8 @@
!macro preInit
SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files\Zoo Modeling App (Nightly)"
!macroend

View File

@ -196,6 +196,7 @@ function ReviewingButton() {
type="submit"
form="review-form"
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-submit"
iconStart={{
icon: 'checkmark',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',
@ -214,6 +215,7 @@ function GatheringArgsButton() {
type="submit"
form="arg-form"
className="w-fit !p-0 rounded-sm hover:shadow"
data-testid="command-bar-continue"
iconStart={{
icon: 'arrowRight',
bgClassName: 'p-1 rounded-sm !bg-primary hover:brightness-110',

View File

@ -20,6 +20,7 @@ import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
import toast from 'react-hot-toast'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
@ -97,6 +98,7 @@ function CommandBarKclInput({
value,
initialVariableName,
})
const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key,
detail: String(roundOff(v.value as number)),
@ -170,7 +172,15 @@ function CommandBarKclInput({
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
e?.preventDefault()
if (!canSubmit || valueNode === null) return
if (!canSubmit || valueNode === null) {
// Gotcha: Our application can attempt to submit a command value before the command bar kcl input is ready. Notify the scene and user.
if (!canSubmit) {
toast.error('Unable to submit command')
} else if (valueNode === null) {
toast.error('Unable to submit undefined command value')
}
return
}
onSubmit(
createNewVariable

View File

@ -2,11 +2,15 @@ import { Dialog } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { CREATE_FILE_URL_PARAM } from 'lib/constants'
const DownloadAppBanner = () => {
const [searchParams] = useSearchParams()
const hasCreateFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
const { settings } = useSettingsAuthContext()
const [isBannerDismissed, setIsBannerDismissed] = useState(
settings.context.app.dismissWebBanner.current
settings.context.app.dismissWebBanner.current || hasCreateFileParam
)
return (

View File

@ -329,11 +329,83 @@ export const ModelingMachineProvider = ({
otherSelections: [],
}
} else if (setSelections.selection && editorManager.isShiftDown) {
// selecting and deselecting multiple objects
/**
* There are two scenarios:
* 1. General case:
* When selecting and deselecting edges,
* faces or segment (during sketch edit)
* we use its artifact ID to identify the selection
* 2. Initial sketch setup:
* The artifact is not yet created
* so we use the codeRef.range
*/
let updatedSelections: typeof selectionRanges.graphSelections
// 1. General case: Artifact exists, use its ID
if (setSelections.selection.artifact?.id) {
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) =>
selection.artifact?.id ===
setSelections.selection?.artifact?.id
)
if (
alreadySelected &&
setSelections.selection?.artifact?.id
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
selection.artifact?.id !==
setSelections.selection?.artifact?.id
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
} else {
// 2. Initial sketch setup: Artifact not yet created use codeRef.range
const selectionRange = JSON.stringify(
setSelections.selection?.codeRef?.range
)
// check if already selected
const alreadySelected = selectionRanges.graphSelections.some(
(selection) => {
const existingRange = JSON.stringify(
selection.codeRef?.range
)
return existingRange === selectionRange
}
)
if (
alreadySelected &&
setSelections.selection?.codeRef?.range
) {
// remove it
updatedSelections = selectionRanges.graphSelections.filter(
(selection) =>
JSON.stringify(selection.codeRef?.range) !==
selectionRange
)
} else {
// add it
updatedSelections = [
...selectionRanges.graphSelections,
setSelections.selection,
]
}
}
selections = {
graphSelections: [
...selectionRanges.graphSelections,
setSelections.selection,
],
graphSelections: updatedSelections,
otherSelections: selectionRanges.otherSelections,
}
}

View File

@ -168,7 +168,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
height: 'auto',
}}
minWidth={200}
maxWidth={800}
maxWidth={window.innerWidth - 10}
handleWrapperClass="sidebar-resize-handles"
handleClasses={{
right:

View File

@ -104,7 +104,7 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings } = useSettingsAuthContext()
useSettingsAuthContext()
const token = useToken()
const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector)
@ -193,14 +193,13 @@ function ProjectMenuPopover({
{
id: 'share-link',
Element: 'button',
children: 'Share link to file',
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
children: 'Share current part (via Zoo link)',
disabled: !(IS_NIGHTLY_OR_DEBUG && findCommand(shareCommandInfo)),
onClick: async () => {
await copyFileShareLink({
token: token ?? '',
code: codeManager.code,
name: project?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
},
},
@ -263,7 +262,7 @@ function ProjectMenuPopover({
as={Fragment}
>
<Popover.Panel
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
className={`z-10 absolute top-full left-0 mt-1 pb-1 w-52 bg-chalkboard-10 dark:bg-chalkboard-90
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
shadow-lg`}
>

View File

@ -30,15 +30,7 @@ import {
FILE_EXT,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { codeManager } from 'lib/singletons'
import {
loadAndValidateSettings,
projectConfigurationToSettingsPayload,
saveSettings,
setSettingsAtLevel,
} from 'lib/settings/settingsUtils'
import { codeManager, kclManager } from 'lib/singletons'
import { Project } from 'lib/project'
type MachineContext<T extends AnyStateMachine> = {
@ -86,7 +78,7 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
setSearchParams(searchParams)
}, [searchParams, setSearchParams])
const {
settings: { context: settings, send: settingsSend },
settings: { context: settings },
} = useSettingsAuthContext()
const [state, send, actor] = useMachine(
@ -132,17 +124,10 @@ const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
clearImportSearchParams()
codeManager.updateCodeStateEditor(input.code || '')
await codeManager.writeToFile()
settingsSend({
type: 'set.modeling.defaultUnit',
data: {
level: 'project',
value: input.units,
},
})
await kclManager.executeCode(true)
return {
message: 'File and units overwritten successfully',
message: 'File overwritten successfully',
fileName: input.name,
projectName: '',
}
@ -392,16 +377,6 @@ const ProjectsContextDesktop = ({
? input.name
: input.name + FILE_EXT
let message = 'File created successfully'
const unitsConfiguration: DeepPartial<Configuration> = {
settings: {
project: {
directory: settings.app.projectDirectory.current,
},
modeling: {
base_unit: input.units,
},
},
}
const needsInterpolated = doesProjectNameNeedInterpolated(projectName)
if (needsInterpolated) {
@ -414,28 +389,10 @@ const ProjectsContextDesktop = ({
// Create the project around the file if newProject
if (input.method === 'newProject') {
await createNewProjectDirectory(
projectName,
input.code,
unitsConfiguration
)
await createNewProjectDirectory(projectName, input.code)
message = `Project "${projectName}" created successfully with link contents`
} else {
let projectPath = window.electron.join(
settings.app.projectDirectory.current,
projectName
)
message = `File "${fileName}" created successfully`
const existingConfiguration = await loadAndValidateSettings(
projectPath
)
const settingsToSave = setSettingsAtLevel(
existingConfiguration.settings,
'project',
projectConfigurationToSettingsPayload(unitsConfiguration)
)
await saveSettings(settingsToSave, projectPath)
}
// Create the file

View File

@ -302,7 +302,7 @@ export const Stream = () => {
return
}
const path = getArtifactOfTypes(
{ key: entity_id, types: ['path', 'solid2d', 'segment'] },
{ key: entity_id, types: ['path', 'solid2d', 'segment', 'helix'] },
engineCommandManager.artifactGraph
)
if (err(path)) {

View File

@ -25,6 +25,18 @@ export class KclPlugin implements PluginValue {
constructor(client: LanguageServerClient) {
this.client = client
// Gotcha: Code can be written into the CodeMirror editor but not propagated to codeManager.code
// because the update function has not run. We need to initialize the codeManager.code when lsp initializes
// because new code could have been written into the editor before the update callback is initialized.
// There appears to be limited ways to safely get the current doc content. This appears to be sync and safe.
const kclLspPlugin = this.client.plugins.find((plugin) => {
return plugin.client.name === 'kcl'
})
if (kclLspPlugin) {
// @ts-ignore Ignoring this private dereference of .view on the plugin. I do not have another helper method that can give me doc string
codeManager.code = kclLspPlugin.view.state.doc.toString()
}
}
// When a doc update needs to be sent to the server, this holds the

View File

@ -6,7 +6,6 @@ import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes'
// For initializing the command arguments, we actually want `method` to be undefined
// so that we don't skip it in the command palette.
@ -37,13 +36,7 @@ export function useCreateFileLinkQuery(
code: base64ToString(
decodeURIComponent(searchParams.get('code') ?? '')
),
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
units:
(baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current,
}
const argDefaultValues: CreateFileSchemaMethodOptional = {
@ -55,7 +48,6 @@ export function useCreateFileLinkQuery(
? settings.context.projects.defaultProjectName.current
: DEFAULT_FILE_NAME,
code: params.code || '',
units: params.units,
method: isDesktop() ? undefined : 'existingProject',
}

View File

@ -58,6 +58,7 @@ export class KclManager {
nonCodeNodes: {},
startNodes: [],
},
trivia: [],
}
private _execState: ExecState = emptyExecState()
private _programMemory: ProgramMemory = ProgramMemory.empty()
@ -239,6 +240,7 @@ export class KclManager {
nonCodeNodes: {},
startNodes: [],
},
trivia: [],
}
}

View File

@ -32,7 +32,7 @@ child_process.spawnSync('git', [
'clone',
'--single-branch',
'--branch',
'achalmers/kw-shell',
'achalmers/kw-appearance',
URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES,
])

View File

@ -128,15 +128,78 @@ describe('Testing findUniqueName', () => {
it('should find a unique name', () => {
const result = findUniqueName(
JSON.stringify([
{ type: 'Identifier', name: 'yo01', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo02', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo03', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo04', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo05', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo06', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo07', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo08', start: 0, end: 0, moduleId: 0 },
{ type: 'Identifier', name: 'yo09', start: 0, end: 0, moduleId: 0 },
{
type: 'Identifier',
name: 'yo01',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo02',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo03',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo04',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo05',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo06',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo07',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo08',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
{
type: 'Identifier',
name: 'yo09',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
},
] satisfies Node<Identifier>[]),
'yo',
2
@ -154,6 +217,7 @@ describe('Testing addSketchTo', () => {
end: 0,
moduleId: 0,
nonCodeMeta: { nonCodeNodes: {}, startNodes: [] },
trivia: [],
},
'yz'
)

View File

@ -278,6 +278,7 @@ export function mutateObjExpProp(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
})
}
}
@ -431,10 +432,11 @@ export function addSweep(
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
const sweep = createCallExpressionStdLibKw(
'sweep',
createIdentifier(profileDeclarator.id.name),
])
[createLabeledArg('path', createIdentifier(pathDeclarator.id.name))]
)
const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
@ -442,8 +444,9 @@ export function addSweep(
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
['arguments', 'CallExpressionKw'],
[0, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
@ -683,6 +686,63 @@ export function addOffsetPlane({
}
}
/**
* Append a helix to the AST
*/
export function addHelix({
node,
revolutions,
angleStart,
counterClockWise,
radius,
axis,
length,
}: {
node: Node<Program>
revolutions: Expr
angleStart: Expr
counterClockWise: boolean
radius: Expr
axis: string
length: Expr
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.HELIX)
const variable = createVariableDeclaration(
name,
createCallExpressionStdLibKw(
'helix',
null, // Not in a pipeline
[
createLabeledArg('revolutions', revolutions),
createLabeledArg('angleStart', angleStart),
createLabeledArg('counterClockWise', createLiteral(counterClockWise)),
createLabeledArg('radius', radius),
createLabeledArg('axis', createLiteral(axis)),
createLabeledArg('length', length),
]
)
)
// TODO: figure out smart insertion than just appending at the end
const argIndex = 0
modifiedAst.body.push(variable)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst,
pathToNode,
}
}
/**
* Return a modified clone of an AST with a named constant inserted into the body
*/
@ -831,6 +891,7 @@ export function createLiteral(value: LiteralValue | number): Node<Literal> {
moduleId: 0,
value,
raw,
trivia: [],
}
}
@ -840,6 +901,7 @@ export function createTagDeclarator(value: string): Node<TagDeclarator> {
start: 0,
end: 0,
moduleId: 0,
trivia: [],
value,
}
@ -851,6 +913,7 @@ export function createIdentifier(name: string): Node<Identifier> {
start: 0,
end: 0,
moduleId: 0,
trivia: [],
name,
}
@ -862,6 +925,7 @@ export function createPipeSubstitution(): Node<PipeSubstitution> {
start: 0,
end: 0,
moduleId: 0,
trivia: [],
}
}
@ -874,11 +938,13 @@ export function createCallExpressionStdLib(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
callee: {
type: 'Identifier',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
name,
},
@ -896,11 +962,13 @@ export function createCallExpressionStdLibKw(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
callee: {
type: 'Identifier',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
name,
},
@ -918,11 +986,13 @@ export function createCallExpression(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
callee: {
type: 'Identifier',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
name,
},
@ -938,6 +1008,7 @@ export function createArrayExpression(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
nonCodeMeta: nonCodeMetaEmpty(),
elements,
@ -952,6 +1023,7 @@ export function createPipeExpression(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
body,
nonCodeMeta: nonCodeMetaEmpty(),
@ -969,12 +1041,14 @@ export function createVariableDeclaration(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
declaration: {
type: 'VariableDeclarator',
start: 0,
end: 0,
moduleId: 0,
trivia: [],
id: createIdentifier(varName),
init,
@ -992,6 +1066,7 @@ export function createObjectExpression(properties: {
start: 0,
end: 0,
moduleId: 0,
trivia: [],
nonCodeMeta: nonCodeMetaEmpty(),
properties: Object.entries(properties).map(([key, value]) => ({
@ -999,6 +1074,7 @@ export function createObjectExpression(properties: {
start: 0,
end: 0,
moduleId: 0,
trivia: [],
key: createIdentifier(key),
value,
@ -1015,6 +1091,7 @@ export function createUnaryExpression(
start: 0,
end: 0,
moduleId: 0,
trivia: [],
operator,
argument,
@ -1031,6 +1108,7 @@ export function createBinaryExpression([left, operator, right]: [
start: 0,
end: 0,
moduleId: 0,
trivia: [],
operator,
left,
@ -1316,6 +1394,7 @@ export async function deleteFromSelection(
varDec.node.init.type === 'PipeExpression') ||
selection.artifact?.type === 'sweep' ||
selection.artifact?.type === 'plane' ||
selection.artifact?.type === 'helix' ||
!selection.artifact // aka expected to be a shell at this point
) {
let extrudeNameToDelete = ''
@ -1323,7 +1402,8 @@ export async function deleteFromSelection(
if (
selection.artifact &&
selection.artifact.type !== 'sweep' &&
selection.artifact.type !== 'plane'
selection.artifact.type !== 'plane' &&
selection.artifact.type !== 'helix'
) {
const varDecName = varDec.node.id.name
traverse(astClone, {
@ -1362,13 +1442,17 @@ export async function deleteFromSelection(
if (!pathToNode) return new Error('Could not find extrude variable')
} else {
pathToNode = selection.codeRef.pathToNode
const extrudeVarDec = getNodeFromPath<VariableDeclarator>(
astClone,
pathToNode,
'VariableDeclarator'
)
if (err(extrudeVarDec)) return extrudeVarDec
extrudeNameToDelete = extrudeVarDec.node.id.name
if (varDec.node.type !== 'VariableDeclarator') {
const callExp = getNodeFromPath<CallExpression>(
astClone,
pathToNode,
'CallExpression'
)
if (err(callExp)) return callExp
extrudeNameToDelete = callExp.node.callee.name
} else {
extrudeNameToDelete = varDec.node.id.name
}
}
const expressionIndex = pathToNode[1][0] as number

View File

@ -46,6 +46,7 @@ export function revolveSketch(
if (err(sketchNode)) return sketchNode
let generatedAxis
let axisDeclaration: PathToNode | null = null
if (axisOrEdge === 'Edge') {
const pathToAxisSelection = getNodePathFromSourceRange(
@ -70,6 +71,13 @@ export function revolveSketch(
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
if (
axisSelection.type === 'segment' ||
axisSelection.type === 'path' ||
axisSelection.type === 'edgeCut'
) {
axisDeclaration = axisSelection.codeRef.pathToNode
}
} else {
generatedAxis = createLiteral(axis)
}
@ -139,18 +147,34 @@ export function revolveSketch(
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
let insertIndex = sketchIndexInBody
if (typeof insertIndex !== 'number')
return new Error('expected insertIndex to be a number')
// If an axis was selected in KCL, find the max index to insert the revolve command
if (axisDeclaration) {
const axisIndexInPathToNode =
axisDeclaration.findIndex((a) => a[0] === 'body') + 1
const axisIndex = axisDeclaration[axisIndexInPathToNode][0]
if (typeof axisIndex !== 'number')
return new Error('expected axisIndex to be a number')
insertIndex = Math.max(insertIndex, axisIndex)
}
clonedAst.body.splice(insertIndex + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
[insertIndex + 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],

View File

@ -1954,6 +1954,7 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
startNodes: [],
nonCodeNodes: [],
},
trivia: [],
},
pathToNode,
}
@ -2534,6 +2535,8 @@ function addTagKw(): addTagFn {
...primaryCallExp,
start: callExpr.node.start,
end: callExpr.node.end,
moduleId: callExpr.node.moduleId,
trivia: callExpr.node.trivia,
})
}

View File

@ -76,6 +76,14 @@ export type ModelingCommandSchema = {
plane: Selections
distance: KclCommandValue
}
Helix: {
revolutions: KclCommandValue
angleStart: KclCommandValue
counterClockWise: boolean
radius: KclCommandValue
axis: string
length: KclCommandValue
}
'change tool': {
tool: SketchTool
}
@ -447,6 +455,53 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
Helix: {
description: 'Create a helix or spiral in 3D about an axis.',
icon: 'helix',
status: 'development',
needsReview: true,
args: {
revolutions: {
inputType: 'kcl',
defaultValue: '1',
required: true,
warningMessage:
'The helix workflow is new and under tested. Please break it and report issues.',
},
angleStart: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE,
required: true,
},
counterClockWise: {
inputType: 'options',
required: true,
options: [
{ name: 'True', isCurrent: false, value: true },
{ name: 'False', isCurrent: true, value: false },
],
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
axis: {
inputType: 'options',
required: true,
options: [
{ name: 'X Axis', isCurrent: true, value: 'X' },
{ name: 'Y Axis', isCurrent: false, value: 'Y' },
{ name: 'Z Axis', isCurrent: false, value: 'Z' },
],
},
length: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
Fillet: {
description: 'Fillet edge',
icon: 'fillet3d',

View File

@ -1,8 +1,6 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine'
export type ProjectsCommandSchema = {
@ -23,7 +21,6 @@ export type ProjectsCommandSchema = {
'Import file from URL': {
name: string
code?: string
units: UnitLength_type
method: 'newProject' | 'existingProject'
projectName?: string
}
@ -157,15 +154,6 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
},
},
units: {
inputType: 'options',
required: false,
skip: true,
options: baseUnitsUnion.map((unit) => ({
name: baseUnitLabels[unit],
value: unit,
})),
},
},
reviewMessage(commandBarContext) {
return isDesktop()

View File

@ -58,6 +58,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SEGMENT: 'seg',
REVOLVE: 'revolve',
PLANE: 'plane',
HELIX: 'helix',
} as const
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`

View File

@ -136,7 +136,7 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
},
{
name: 'share-file-link',
displayName: 'Share file',
displayName: 'Share current part (via Zoo link)',
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
description: 'Create a link that contains a copy of the current file.',
groupId: 'code',
@ -147,7 +147,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
token: commandProps.authToken,
code: codeManager.code,
name: commandProps.projectData.project?.name || '',
units: commandProps.settings.defaultUnit,
}).catch(reportRejection)
},
},

View File

@ -5,13 +5,12 @@ describe(`link creation tests`, () => {
test(`createCreateFileUrl happy path`, async () => {
const code = `extrusionDistance = 12`
const name = `test`
const units = `mm`
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true`
const result = createCreateFileUrl({ code, name, units })
const result = createCreateFileUrl({ code, name })
expect(result.toString()).toBe(expectedLink)
})
})

View File

@ -1,4 +1,3 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
import { stringToBase64 } from './base64'
import { VITE_KC_API_BASE_URL, VITE_KC_SITE_APP_URL } from 'env'
@ -7,7 +6,6 @@ import { err } from './trap'
export interface FileLinkParams {
code: string
name: string
units: UnitLength_type
}
export async function copyFileShareLink(
@ -46,12 +44,11 @@ export async function copyFileShareLink(
* With the additional step of asking the user if they want to
* open the URL in the desktop app.
*/
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
export function createCreateFileUrl({ code, name }: FileLinkParams) {
let origin = VITE_KC_SITE_APP_URL
const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: String(true),
name,
units,
code: stringToBase64(code),
[ASK_TO_OPEN_QUERY_PARAM]: String(true),
})

View File

@ -290,9 +290,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
],
{
id: 'helix',
onClick: () => console.error('Helix not yet implemented'),
onClick: () => {
commandBarActor.send({
type: 'Find and select command',
data: { name: 'Helix', groupId: 'modeling' },
})
},
hotkey: 'H',
icon: 'helix',
status: 'kcl-only',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Helix',
description: 'Create a helix or spiral in 3D about an axis.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],

View File

@ -9,6 +9,8 @@ import {
getCalculatedKclExpressionValue,
programMemoryFromVariables,
} from './kclHelpers'
import { parse, resultIsOk } from 'lang/wasm'
import { err } from 'lib/trap'
const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -50,7 +52,20 @@ export function useCalculateKclExpression({
bodyPath: [],
})
const [valueNode, setValueNode] = useState<Expr | null>(null)
const [calcResult, setCalcResult] = useState('NAN')
// Gotcha: If we do not attempt to parse numeric literals instantly it means that there is an async action to verify
// the value is good. This means all E2E tests have a race condition on when they can hit "next" in the command bar.
// Most scenarios automatically pass a numeric literal. We can try to parse that first, otherwise make it go through the slow
// async method.
// If we pass in numeric literals, we should instantly parse them, they have nothing to do with application memory
const _code_value = `const __result__ = ${value}`
const codeValueParseResult = parse(_code_value)
let isValueParsable = true
if (err(codeValueParseResult) || !resultIsOk(codeValueParseResult)) {
isValueParsable = false
}
const initialCalcResult: number | string =
Number.isNaN(Number(value)) || !isValueParsable ? 'NAN' : value
const [calcResult, setCalcResult] = useState(initialCalcResult)
const [newVariableName, setNewVariableName] = useState('')
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)

View File

@ -42,6 +42,7 @@ import {
} from 'components/Toolbar/EqualLength'
import { revolveSketch } from 'lang/modifyAst/addRevolve'
import {
addHelix,
addOffsetPlane,
addSweep,
deleteFromSelection,
@ -276,6 +277,7 @@ export type ModelingMachineEvent =
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
| {
@ -1615,6 +1617,73 @@ export const modelingMachine = setup({
}
}
),
helixAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Helix'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const {
revolutions,
angleStart,
counterClockWise,
radius,
axis,
length,
} = input
for (const variable of [revolutions, angleStart, radius, length]) {
// Insert the variable if it exists
if (
'variableName' in variable &&
variable.variableName &&
variable.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(
variable.insertIndex,
0,
variable.variableDeclarationAst
)
ast.body = newBody
}
}
const valueOrVariable = (variable: KclCommandValue) =>
'variableName' in variable
? variable.variableIdentifierAst
: variable.valueAst
const result = addHelix({
node: ast,
revolutions: valueOrVariable(revolutions),
angleStart: valueOrVariable(angleStart),
counterClockWise,
radius: valueOrVariable(radius),
axis,
length: valueOrVariable(length),
})
const updateAstResult = await kclManager.updateAst(
result.modifiedAst,
true,
{
focusPath: [result.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
sweepAstMod: fromPromise(
async ({
input,
@ -1974,6 +2043,11 @@ export const modelingMachine = setup({
reenter: true,
},
Helix: {
target: 'Applying helix',
reenter: true,
},
'Prompt-to-edit': 'Applying Prompt-to-edit',
},
@ -2734,6 +2808,19 @@ export const modelingMachine = setup({
},
},
'Applying helix': {
invoke: {
src: 'helixAstMod',
id: 'helixAstMod',
input: ({ event }) => {
if (event.type !== 'Helix') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying sweep': {
invoke: {
src: 'sweepAstMod',

View File

@ -306,7 +306,6 @@ export const projectsMachine = setup({
return {
code: '',
name: '',
units: 'mm',
method: 'existingProject',
projects: context.projects,
}
@ -314,7 +313,6 @@ export const projectsMachine = setup({
return {
code: event.data.code || '',
name: event.data.name,
units: event.data.units,
method: event.data.method,
projectName: event.data.projectName,
projects: context.projects,

View File

@ -40,7 +40,6 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// default vite values based on mode
process.env.NODE_ENV ??= viteEnv.MODE
process.env.DEV ??= viteEnv.DEV + ''
process.env.BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL
@ -94,12 +93,11 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
}
// Deep Link: Case of a cold start from Windows or Linux
if (
!pathToOpen &&
process.argv.length > 1 &&
process.argv[1].indexOf(ZOO_STUDIO_PROTOCOL + '://') > -1
) {
pathToOpen = process.argv[1]
const zooProtocolArg = process.argv.find((a) =>
a.startsWith(ZOO_STUDIO_PROTOCOL + '://')
)
if (!pathToOpen && zooProtocolArg) {
pathToOpen = zooProtocolArg
console.log('Retrieved deep link from argv', pathToOpen)
}
@ -329,6 +327,7 @@ ipcMain.handle('kittycad', (event, data) => {
)(data.args)
})
// Used to find other devices on the local network, e.g. 3D printers, CNC machines, etc.
ipcMain.handle('find_machine_api', () => {
const timeoutAfterMs = 5000
return new Promise((resolve, reject) => {
@ -339,7 +338,6 @@ ipcMain.handle('find_machine_api', () => {
console.error(error)
resolve(null)
})
console.log('Looking for machine API...')
bonjourEt.find(
{ protocol: 'tcp', type: 'machine-api' },
(service: Service) => {

View File

@ -26,7 +26,7 @@ export default function Units() {
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
<div
className={
'pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<SettingsSection
@ -72,9 +72,7 @@ export default function Units() {
</SettingsSection>
<OnboardingButtons
currentSlug={onboardingPaths.CAMERA}
dismiss={dismiss}
next={next}
nextText="Next: Streaming"
dismissClassName="right-auto left-full"
/>
</div>
</div>

View File

@ -1,19 +1,17 @@
import usePlatform from 'hooks/usePlatform'
import { OnboardingButtons, kbdClasses, useDismiss, useNextClick } from '.'
import { OnboardingButtons, kbdClasses } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { hotkeyDisplay } from 'lib/hotkeyWrapper'
import { COMMAND_PALETTE_HOTKEY } from 'components/CommandBar/CommandBar'
export default function CmdK() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.USER_MENU)
const platformName = usePlatform()
return (
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h2 className="text-2xl font-bold">Command Bar</h2>
@ -38,12 +36,7 @@ export default function CmdK() {
. You can control settings, authentication, and file management from
the command bar, as well as a growing number of modeling commands.
</p>
<OnboardingButtons
currentSlug={onboardingPaths.COMMAND_K}
dismiss={dismiss}
next={next}
nextText="Next: User Menu"
/>
<OnboardingButtons currentSlug={onboardingPaths.COMMAND_K} />
</div>
</div>
)

View File

@ -1,22 +1,14 @@
import {
kbdClasses,
OnboardingButtons,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { kbdClasses, OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function OnboardingCodeEditor() {
useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PARAMETRIC_MODELING)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto">
@ -73,12 +65,7 @@ export default function OnboardingCodeEditor() {
pressing <kbd className={kbdClasses}>Shift + C</kbd>.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.EDITOR}
dismiss={dismiss}
next={next}
nextText="Next: Parametric Modeling"
/>
<OnboardingButtons currentSlug={onboardingPaths.EDITOR} />
</div>
</div>
)

View File

@ -1,16 +1,13 @@
import { APP_NAME } from 'lib/constants'
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Export() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.SKETCHING)
return (
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -52,12 +49,7 @@ export default function Export() {
!
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.EXPORT}
next={next}
dismiss={dismiss}
nextText="Next: Sketching"
/>
<OnboardingButtons currentSlug={onboardingPaths.EXPORT} />
</div>
</div>
)

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { useEffect } from 'react'
import { useModelingContext } from 'hooks/useModelingContext'
import { APP_NAME } from 'lib/constants'
@ -7,7 +7,6 @@ import { sceneInfra } from 'lib/singletons'
export default function FutureWork() {
const { send } = useModelingContext()
const dismiss = useDismiss()
// Reset the code, the camera, and the modeling state
useDemoCode()
@ -19,7 +18,7 @@ export default function FutureWork() {
return (
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
<div className="max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<div className="relative max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<h1 className="text-2xl font-bold">Future Work</h1>
<p className="my-4">
We have curves, cuts, multi-profile sketch mode, and many more CAD
@ -59,9 +58,6 @@ export default function FutureWork() {
<OnboardingButtons
currentSlug={onboardingPaths.FUTURE_WORK}
className="mt-6"
dismiss={dismiss}
next={dismiss}
nextText="Finish"
/>
</div>
</div>

View File

@ -1,23 +1,15 @@
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
useDismiss,
useNextClick,
} from '.'
import { OnboardingButtons, kbdClasses, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { bracketWidthConstantLine } from 'lib/exampleKcl'
export default function OnboardingInteractiveNumbers() {
useDemoCode()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.COMMAND_K)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
@ -88,12 +80,7 @@ export default function OnboardingInteractiveNumbers() {
your ideas for how to make it better.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.INTERACTIVE_NUMBERS}
dismiss={dismiss}
next={next}
nextText="Next: Command Bar"
/>
<OnboardingButtons currentSlug={onboardingPaths.INTERACTIVE_NUMBERS} />
</div>
</div>
)

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
@ -8,13 +8,12 @@ import { isDesktop } from 'lib/isDesktop'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons'
import { APP_NAME } from 'lib/constants'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { IndexLoaderData } from 'lib/types'
import { PATHS } from 'lib/paths'
import { useFileContext } from 'hooks/useFileContext'
import { useLspContext } from 'components/LspProvider'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
/**
* Show either a welcome screen or a warning screen
@ -39,7 +38,7 @@ interface OnboardingResetWarningProps {
function OnboardingResetWarning(props: OnboardingResetWarningProps) {
return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
{!isDesktop() ? (
<OnboardingWarningWeb {...props} />
) : (
@ -52,7 +51,6 @@ function OnboardingResetWarning(props: OnboardingResetWarningProps) {
function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
const navigate = useNavigate()
const dismiss = useDismiss()
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { context: fileContext } = useFileContext()
const { onProjectClose, onProjectOpen } = useLspContext()
@ -81,17 +79,28 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
</section>
<OnboardingButtons
className="mt-6"
dismiss={dismiss}
next={toSync(onAccept, reportRejection)}
nextText="Make a new project"
onNextOverride={() => {
onAccept().catch(reportRejection)
}}
/>
</>
)
}
function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
const dismiss = useDismiss()
useEffect(() => {
async function beforeNavigate() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}
return () => {
beforeNavigate().catch(reportRejection)
}
}, [])
return (
<>
<h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10">
@ -101,19 +110,7 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
We see you have some of your own code written in this project. Please
save it somewhere else before continuing the onboarding.
</p>
<OnboardingButtons
className="mt-6"
dismiss={dismiss}
next={toSync(async () => {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
await codeManager.writeToFile()
await kclManager.executeCode(true)
props.setShouldShowWarning(false)
}, reportRejection)}
nextText="Overwrite code and continue"
/>
<OnboardingButtons className="mt-6" />
</>
)
}
@ -136,12 +133,10 @@ function OnboardingIntroductionInner() {
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
@ -192,9 +187,6 @@ function OnboardingIntroductionInner() {
<OnboardingButtons
currentSlug={onboardingPaths.INDEX}
className="mt-6"
dismiss={dismiss}
next={next}
nextText="Mouse Controls"
/>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { OnboardingButtons, useDemoCode, useDismiss, useNextClick } from '.'
import { OnboardingButtons, useDemoCode } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
@ -21,14 +21,12 @@ export default function OnboardingParametricModeling() {
(theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.INTERACTIVE_NUMBERS)
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
@ -77,12 +75,7 @@ export default function OnboardingParametricModeling() {
</figcaption>
</figure>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.PARAMETRIC_MODELING}
dismiss={dismiss}
next={next}
nextText="Next: Interactive Numbers"
/>
<OnboardingButtons currentSlug={onboardingPaths.PARAMETRIC_MODELING} />
</div>
</div>
)

View File

@ -1,17 +1,15 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { isDesktop } from 'lib/isDesktop'
export default function ProjectMenu() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EXPORT)
const onDesktop = isDesktop()
return (
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -57,12 +55,7 @@ export default function ProjectMenu() {
</>
)}
</section>
<OnboardingButtons
currentSlug={onboardingPaths.PROJECT_MENU}
next={next}
dismiss={dismiss}
nextText="Next: Export"
/>
<OnboardingButtons currentSlug={onboardingPaths.PROJECT_MENU} />
</div>
</div>
)

View File

@ -1,12 +1,9 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect } from 'react'
import { codeManager, kclManager } from 'lib/singletons'
export default function Sketching() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.FUTURE_WORK)
useEffect(() => {
async function clearEditor() {
// We do want to update both the state and editor here.
@ -22,7 +19,7 @@ export default function Sketching() {
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h1 className="text-2xl font-bold">Sketching</h1>
@ -45,9 +42,6 @@ export default function Sketching() {
<OnboardingButtons
currentSlug={onboardingPaths.SKETCHING}
className="mt-6"
next={next}
dismiss={dismiss}
nextText="Next: Future Work"
/>
</div>
</div>

View File

@ -1,15 +1,12 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
export default function Streaming() {
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.EDITOR)
return (
<div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto">
@ -44,9 +41,7 @@ export default function Streaming() {
</section>
<OnboardingButtons
currentSlug={onboardingPaths.STREAMING}
dismiss={dismiss}
next={next}
nextText="Next: Code Editor"
dismissClassName="right-auto left-full"
/>
</div>
</div>

View File

@ -1,12 +1,10 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { OnboardingButtons } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react'
import { useUser } from 'machines/appMachine'
export default function UserMenu() {
const user = useUser()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false)
const errorOrNoImage = !user?.image || avatarErrored
@ -32,7 +30,7 @@ export default function UserMenu() {
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div
className={
'pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
@ -48,12 +46,7 @@ export default function UserMenu() {
only apply to the current project.
</p>
</section>
<OnboardingButtons
currentSlug={onboardingPaths.USER_MENU}
dismiss={dismiss}
next={next}
nextText="Next: Project Menu"
/>
<OnboardingButtons currentSlug={onboardingPaths.USER_MENU} />
</div>
</div>
)

View File

@ -26,6 +26,9 @@ import { reportRejection } from 'lib/trap'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { EngineConnectionStateType } from 'lang/std/engineConnection'
import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip'
import { commandBarActor } from 'machines/commandBarMachine'
export const kbdClasses =
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
@ -163,58 +166,99 @@ export function useStepNumber(
: onboardingRoutes.findIndex(
(r) => r.path === makeUrlPathRelative(slug)
) + 1
: undefined
: 1
}
export function OnboardingButtons({
next,
nextText,
dismiss,
currentSlug,
className,
dismissClassName,
onNextOverride,
...props
}: {
next: () => void
nextText?: string
dismiss: () => void
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
className?: string
dismissClassName?: string
onNextOverride?: () => void
} & React.HTMLAttributes<HTMLDivElement>) {
const dismiss = useDismiss()
const stepNumber = useStepNumber(currentSlug)
const previousStep =
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
const goToPrevious = useNextClick(
onboardingPaths.INDEX + (previousStep?.path ?? '')
)
const nextStep =
!stepNumber || stepNumber === onboardingRoutes.length
? null
: onboardingRoutes[stepNumber]
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
return (
<div
className={'flex items-center justify-between ' + (className ?? '')}
{...props}
>
<ActionButton
Element="button"
<>
<button
onClick={dismiss}
iconStart={{
icon: 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
className={
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
dismissClassName
}
data-testid="onboarding-dismiss"
>
Dismiss
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={next}
iconStart={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
<CustomIcon
name="close"
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
/>
<Tooltip position="bottom" delay={500}>
Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
</Tooltip>
</button>
<div
className={'flex items-center justify-between ' + (className ?? '')}
{...props}
>
{nextText ?? 'Next'}
</ActionButton>
</div>
<ActionButton
Element="button"
onClick={() =>
previousStep?.path || previousStep?.index
? goToPrevious()
: dismiss()
}
iconStart={{
icon: previousStep ? 'arrowLeft' : 'close',
className: 'text-chalkboard-10',
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
}}
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
data-testid="onboarding-prev"
>
{previousStep ? `Back` : 'Dismiss'}
</ActionButton>
{stepNumber !== undefined && (
<p className="font-mono text-xs text-center m-0">
{stepNumber} / {onboardingRoutes.length}
</p>
)}
<ActionButton
autoFocus
Element="button"
onClick={() => {
if (nextStep?.path) {
onNextOverride ? onNextOverride() : goToNext()
} else {
dismiss()
}
}}
iconStart={{
icon: nextStep ? 'arrowRight' : 'checkmark',
bgClassName: 'dark:bg-chalkboard-80',
}}
className="dark:hover:bg-chalkboard-80/50"
data-testid="onboarding-next"
>
{nextStep ? `Next` : 'Finish'}
</ActionButton>
</div>
</>
)
}

View File

@ -32,7 +32,8 @@ export const PACKAGE_NAME = isDesktop()
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0'
export const IS_NIGHTLY_OR_DEBUG =
IS_NIGHTLY || APP_VERSION === '0.0.0' || APP_VERSION === '11.22.33'
export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.35"
version = "0.1.36"
dependencies = [
"Inflector",
"anyhow",
@ -1712,7 +1712,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.34"
version = "0.2.35"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -3273,9 +3273,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"indexmap 2.7.1",
"itoa",

View File

@ -15,7 +15,7 @@ gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
lazy_static = "1.5.0"
serde_json = "1.0.135"
serde_json = "1.0.138"
tokio = { version = "1.41.1", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -22,10 +22,15 @@ copy-exec-test-into-sim-test test_name:
zoo kcl fmt -w kcl/tests/{{test_name}}/input.kcl
just new-sim-test {{test_name}}
# Create a new KCL deterministic simulation test case.
# Create a new, empty KCL deterministic simulation test case.
new-sim-test test_name render_to_png="true":
mkdir kcl/tests/{{test_name}}
touch kcl/tests/{{test_name}}/input.kcl
# Add the various tests for this new test case.
cat kcl/tests/simtest.tmpl | sed "s/TEST_NAME_HERE/{{test_name}}/" | sed "s/RENDER_TO_PNG/{{render_to_png}}/" >> kcl/src/simulation_tests.rs
# Run a KCL deterministic simulation test case and accept output.
run-sim-test test_name:
# Run all the tests for the first time, in the right order.
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::parse
{{cita}} -p kcl-lib -- simulation_tests::{{test_name}}::unparse

View File

@ -11,7 +11,7 @@ hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
serde_json = "1.0.138"
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
[lints]

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.34"
version = "0.2.35"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -56,7 +56,7 @@ schemars = { version = "0.8.17", features = [
"preserve_order",
] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
serde_json = "1.0.138"
sha2 = "0.10.8"
tabled = { version = "0.15.0", optional = true }
thiserror = "2.0.0"

View File

@ -118,7 +118,7 @@ impl StdLibFnArg {
} else if self.type_ == "KclValue" && self.required {
return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3"))));
}
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn)
self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn, &self.name)
.map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}"))))
}
@ -136,6 +136,7 @@ impl StdLibFnArg {
schema: &schemars::schema::Schema,
index: usize,
in_keyword_fn: bool,
name: &str,
) -> Result<Option<(usize, String)>> {
match schema {
schemars::schema::Schema::Object(o) => {
@ -149,6 +150,10 @@ impl StdLibFnArg {
return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000"))));
}
if name == "color" {
let snippet = format!("${{{}:\"#ff0000\"}}", index);
return Ok(Some((index, snippet)));
}
if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") {
if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) {
return Ok(None);
@ -192,13 +197,9 @@ impl StdLibFnArg {
continue;
}
if prop_name == "color" {
fn_docs.push_str(&format!("\t{} = ${{{}:\"#ff0000\"}},\n", prop_name, i));
i += 1;
continue;
}
if let Some((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(prop, i, false)? {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(prop, i, false, name)?
{
fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet));
i = new_index + 1;
}
@ -223,7 +224,8 @@ impl StdLibFnArg {
.get_autocomplete_snippet_from_schema(
items,
index + (v as usize),
in_keyword_fn
in_keyword_fn,
name
)
.unwrap()
.unwrap()
@ -238,7 +240,7 @@ impl StdLibFnArg {
index,
format!(
"[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1
),
@ -250,7 +252,7 @@ impl StdLibFnArg {
index,
format!(
"[{}]",
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)?
.ok_or_else(|| anyhow::anyhow!("expected snippet"))?
.1
),
@ -293,7 +295,7 @@ impl StdLibFnArg {
return Ok(Some((index, parsed_enum_values[0].to_string())));
} else if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{
i = new_index + 1;
fn_docs.push_str(&snippet);
@ -302,7 +304,7 @@ impl StdLibFnArg {
} else if let Some(items) = &subschemas.any_of {
if let Some(item) = items.iter().next() {
if let Some((new_index, snippet)) =
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn)?
self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)?
{
i = new_index + 1;
fn_docs.push_str(&snippet);
@ -1018,12 +1020,7 @@ mod tests {
let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"appearance({
color = ${0:"#
.to_owned()
+ "\"#"
+ r#"ff0000"},
}, ${1:%})${}"#
r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"#
);
}
@ -1038,12 +1035,7 @@ mod tests {
fn get_autocomplete_snippet_sweep() {
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
let snippet = sweep_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
snippet,
r#"sweep({
path = ${0:sketch000},
}, ${1:%})${}"#
);
assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})${}"#);
}
#[test]

View File

@ -5,7 +5,8 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{
execution::{ArtifactCommand, ArtifactGraph, Operation},
lsp::IntoDiagnostic,
source_range::{ModuleId, SourceRange},
source_range::SourceRange,
ModuleId,
};
/// How did the KCL execution fail

View File

@ -10,6 +10,7 @@ use crate::{
pub(crate) const SETTINGS: &str = "settings";
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
pub(super) const NO_PRELUDE: &str = "no_prelude";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope {
@ -23,7 +24,7 @@ pub(super) fn expect_properties<'a>(
) -> Result<&'a [Node<ObjectProperty>], KclError> {
match annotation {
NonCodeValue::Annotation { name, properties } => {
assert_eq!(name.name, for_key);
assert_eq!(name.as_ref().unwrap().name, for_key);
Ok(&**properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),

View File

@ -269,6 +269,17 @@ pub struct EdgeCutEdge {
pub surface_id: ArtifactId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub struct Helix {
pub id: ArtifactId,
/// The axis of the helix. Currently this is always an edge ID, but we may
/// add axes to the graph.
pub axis_id: Option<ArtifactId>,
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(tag = "type", rename_all = "camelCase")]
@ -295,6 +306,7 @@ pub enum Artifact {
SweepEdge(SweepEdge),
EdgeCut(EdgeCut),
EdgeCutEdge(EdgeCutEdge),
Helix(Helix),
}
impl Artifact {
@ -312,6 +324,7 @@ impl Artifact {
Artifact::SweepEdge(a) => a.id,
Artifact::EdgeCut(a) => a.id,
Artifact::EdgeCutEdge(a) => a.id,
Artifact::Helix(a) => a.id,
}
}
@ -331,6 +344,7 @@ impl Artifact {
Artifact::SweepEdge(_) => None,
Artifact::EdgeCut(a) => Some(&a.code_ref),
Artifact::EdgeCutEdge(_) => None,
Artifact::Helix(a) => Some(&a.code_ref),
}
}
@ -350,6 +364,7 @@ impl Artifact {
Artifact::SweepEdge(_) => Some(new),
Artifact::EdgeCut(a) => a.merge(new),
Artifact::EdgeCutEdge(_) => Some(new),
Artifact::Helix(_) => Some(new),
}
}
}
@ -924,6 +939,25 @@ fn artifacts_to_update(
}
return Ok(return_arr);
}
ModelingCmd::EntityMakeHelixFromParams(_) => {
let return_arr = vec![Artifact::Helix(Helix {
id,
axis_id: None,
code_ref: CodeRef { range, path_to_node },
})];
return Ok(return_arr);
}
ModelingCmd::EntityMakeHelixFromEdge(helix) => {
let edge_id = ArtifactId::new(helix.edge_id);
let return_arr = vec![Artifact::Helix(Helix {
id,
axis_id: Some(edge_id),
code_ref: CodeRef { range, path_to_node },
})];
// We could add the reverse graph edge connecting from the edge to
// the helix here, but it's not useful right now.
return Ok(return_arr);
}
_ => {}
}

View File

@ -79,6 +79,7 @@ impl Artifact {
Artifact::SweepEdge(a) => vec![a.seg_id, a.sweep_id],
Artifact::EdgeCut(a) => vec![a.consumed_edge_id],
Artifact::EdgeCutEdge(a) => vec![a.edge_cut_id],
Artifact::Helix(a) => a.axis_id.map(|id| vec![id]).unwrap_or_default(),
}
}
@ -157,6 +158,10 @@ impl Artifact {
// Note: Don't include these since they're parents: edge_cut_id.
vec![a.surface_id]
}
Artifact::Helix(_) => {
// Note: Don't include these since they're parents: axis_id.
Vec::new()
}
}
}
}
@ -224,7 +229,8 @@ impl ArtifactGraph {
| Artifact::Cap(_)
| Artifact::SweepEdge(_)
| Artifact::EdgeCut(_)
| Artifact::EdgeCutEdge(_) => false,
| Artifact::EdgeCutEdge(_)
| Artifact::Helix(_) => false,
};
if !grouped {
ungrouped.push(id);
@ -341,6 +347,14 @@ impl ArtifactGraph {
Artifact::EdgeCutEdge(_edge_cut_edge) => {
writeln!(output, "{prefix}{}[EdgeCutEdge]", id)?;
}
Artifact::Helix(helix) => {
writeln!(
output,
"{prefix}{}[\"Helix<br>{:?}\"]",
id,
code_ref_display(&helix.code_ref)
)?;
}
}
Ok(())
}
@ -522,6 +536,9 @@ impl ArtifactGraph {
Artifact::EdgeCutEdge(_edge_cut_edge) => {
writeln!(output, "{prefix}EdgeCutEdge")?;
}
Artifact::Helix(_) => {
writeln!(output, "{prefix}Helix")?;
}
}
if ids_seen.contains(&artifact.id()) {

View File

@ -128,7 +128,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
properties: new_properties,
},
) => {
name.digest == new_name.digest
name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
&& properties
.as_ref()
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())

View File

@ -10,17 +10,17 @@ use crate::{
annotations,
cad_op::{OpArg, Operation},
state::ModuleState,
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory,
TagEngineInfo, TagIdentifier,
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, TagEngineInfo,
TagIdentifier,
},
fs::FileSystem,
modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator,
},
source_range::{ModuleId, SourceRange},
source_range::SourceRange,
std::{
args::{Arg, KwArgs},
FunctionKind,
@ -38,7 +38,8 @@ impl ExecutorContext {
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
scope: annotations::AnnotationScope,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
) -> Result<bool, KclError> {
let mut no_prelude = false;
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
if scope == annotations::AnnotationScope::Module {
@ -48,7 +49,7 @@ impl ExecutorContext {
.settings
.update_from_annotation(annotation, source_range)?;
let new_units = exec_state.length_unit();
if old_units != new_units {
if !self.engine.execution_kind().is_isolated() && old_units != new_units {
self.engine.set_units(new_units.into(), source_range).await?;
}
} else {
@ -58,9 +59,19 @@ impl ExecutorContext {
}));
}
}
if annotation.annotation_name() == Some(annotations::NO_PRELUDE) {
if scope == annotations::AnnotationScope::Module {
no_prelude = true;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Prelude can only be skipped at the top level scope of a file".to_owned(),
source_ranges: vec![source_range],
}));
}
}
// TODO warn on unknown annotations
}
Ok(())
Ok(no_prelude)
}
/// Execute an AST's program.
@ -71,22 +82,32 @@ impl ExecutorContext {
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValue>, KclError> {
self.handle_annotations(
program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
annotations::AnnotationScope::Module,
exec_state,
)
.await?;
if body_type == BodyType::Root {
let _no_prelude = self
.handle_annotations(
program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
annotations::AnnotationScope::Module,
exec_state,
)
.await?;
}
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {
for (i, statement) in program.body.iter().enumerate() {
match statement {
BodyItem::ImportStatement(import_stmt) => {
if body_type != BodyType::Root {
return Err(KclError::Semantic(KclErrorDetails {
message: "Imports are only supported at the top-level of a file.".to_owned(),
source_ranges: vec![import_stmt.into()],
}));
}
let source_range = SourceRange::from(import_stmt);
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
@ -178,6 +199,14 @@ impl ExecutorContext {
let source_range = SourceRange::from(&variable_declaration.declaration.init);
let metadata = Metadata { source_range };
let _meta_nodes = if i == 0 {
&program.non_code_meta.start_nodes
} else if let Some(meta) = program.non_code_meta.non_code_nodes.get(&(i - 1)) {
meta
} else {
&Vec::new()
};
let memory_item = self
.execute_expr(
&variable_declaration.declaration.init,
@ -231,63 +260,45 @@ impl ExecutorContext {
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
match path {
ImportPath::Kcl { filename } => {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(filename)
} else {
std::path::PathBuf::from(filename)
};
ImportPath::Kcl { .. } => {
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
if exec_state.mod_local.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.mod_local
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![source_range],
}));
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
}
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
let id = exec_state.next_module_id();
let source = resolved_path.source(&self.fs, source_range).await?;
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let repr = ModuleRepr::Kcl(parsed);
Ok(exec_state.add_module(id, resolved_path, repr))
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
Ok(id)
}
ImportPath::Foreign { path } => {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(path)
};
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
ImportPath::Foreign { .. } => {
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
let repr = ModuleRepr::Foreign(geom);
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
Ok(exec_state.add_module(id, resolved_path, repr))
let id = exec_state.next_module_id();
let geom =
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
.await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
Ok(id)
}
ImportPath::Std { .. } => {
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
let id = exec_state.next_module_id();
let source = resolved_path.source(&self.fs, source_range).await?;
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err().unwrap();
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
Ok(id)
}
i => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unsupported import: `{i}`"),
source_ranges: vec![source_range],
})),
}
}
@ -307,22 +318,20 @@ impl ExecutorContext {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.mod_local
.global
.mod_loader
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
info.path.display()
info.path
),
source_ranges: vec![source_range],
})),
ModuleRepr::Kcl(program) => {
let mut local_state = ModuleState {
import_stack: exec_state.mod_local.import_stack.clone(),
..ModuleState::new(&self.settings)
};
local_state.import_stack.push(info.path.clone());
let mut local_state = ModuleState::new(&self.settings);
exec_state.global.mod_loader.enter_module(&info.path);
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
let original_execution = self.engine.replace_execution_kind(exec_kind);
@ -332,7 +341,8 @@ impl ExecutorContext {
let new_units = exec_state.length_unit();
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
if new_units != old_units {
exec_state.global.mod_loader.leave_module(&info.path);
if !exec_kind.is_isolated() && new_units != old_units {
self.engine.set_units(old_units.into(), Default::default()).await?;
}
self.engine.replace_execution_kind(original_execution);
@ -345,7 +355,7 @@ impl ExecutorContext {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {}: {}",
info.path.display(),
info.path,
err.message()
),
source_ranges: vec![source_range],
@ -1759,6 +1769,7 @@ mod test {
start: 0,
end: 0,
module_id: ModuleId::default(),
trivia: Vec::new(),
},
return_type: None,
digest: None,

View File

@ -9,7 +9,9 @@ use cache::OldAstState;
pub use cad_op::Operation;
pub use exec_ast::FunctionParam;
pub use geometry::*;
pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine, ZOO_COORD_SYSTEM};
pub(crate) use import::{
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
};
use indexmap::IndexMap;
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
use kcmc::{
@ -34,7 +36,7 @@ use crate::{
fs::FileManager,
parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
source_range::SourceRange,
std::{args::Arg, StdLib},
ExecError, KclErrorWithOutputs,
};
@ -162,25 +164,6 @@ pub enum BodyType {
Block,
}
/// Info about a module. Right now, this is pretty minimal. We hope to cache
/// modules here in the future.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ModuleInfo {
/// The ID of the module.
id: ModuleId,
/// Absolute path of the module's source file.
path: std::path::PathBuf,
repr: ModuleRepr,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ModuleRepr {
Root,
Kcl(Node<Program>),
Foreign(import::PreImportedGeometry),
}
/// Metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
#[ts(export)]
@ -791,7 +774,7 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::errors::KclErrorDetails;
use crate::{errors::KclErrorDetails, ModuleId};
/// Convenience function to get a JSON value from memory and unwrap.
#[track_caller]

View File

@ -9,10 +9,11 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{
annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings,
KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
KclValue, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr},
parsing::ast::types::NonCodeValue,
source_range::{ModuleId, SourceRange},
source_range::SourceRange,
};
/// State for executing a program.
@ -29,7 +30,7 @@ pub struct GlobalState {
/// The stable artifact ID generator.
pub id_generator: IdGenerator,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Output map of UUIDs to artifacts.
@ -45,6 +46,8 @@ pub struct GlobalState {
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub artifact_graph: ArtifactGraph,
/// Module loader.
pub mod_loader: ModuleLoader,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
@ -59,9 +62,6 @@ pub struct ModuleState {
pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module.
pub module_exports: Vec<String>,
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
@ -124,15 +124,21 @@ impl ExecState {
self.global.artifacts.insert(id, artifact);
}
pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
pub(super) fn next_module_id(&self) -> ModuleId {
ModuleId::from_usize(self.global.path_to_source_id.len())
}
pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
self.global.path_to_source_id.get(path).cloned()
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
self.global.path_to_source_id.insert(path.clone(), id);
let module_info = ModuleInfo { id, repr, path };
self.global.module_infos.insert(id, module_info);
id
}
pub fn length_unit(&self) -> UnitLen {
@ -154,6 +160,7 @@ impl GlobalState {
artifact_commands: Default::default(),
artifact_responses: Default::default(),
artifact_graph: Default::default(),
mod_loader: Default::default(),
};
let root_id = ModuleId::default();
@ -162,11 +169,11 @@ impl GlobalState {
root_id,
ModuleInfo {
id: root_id,
path: root_path.clone(),
path: ModulePath::Local(root_path.clone()),
repr: ModuleRepr::Root,
},
);
global.path_to_source_id.insert(root_path, root_id);
global.path_to_source_id.insert(ModulePath::Local(root_path), root_id);
global
}
}
@ -178,7 +185,6 @@ impl ModuleState {
dynamic_state: Default::default(),
pipe_value: Default::default(),
module_exports: Default::default(),
import_stack: Default::default(),
operations: Default::default(),
settings: MetaSettings {
default_length_units: exec_settings.units.into(),

View File

@ -65,6 +65,7 @@ mod fs;
pub mod lint;
mod log;
mod lsp;
mod modules;
mod parsing;
mod settings;
#[cfg(test)]
@ -87,9 +88,10 @@ pub use lsp::{
copilot::Backend as CopilotLspBackend,
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
};
pub use modules::ModuleId;
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::{ModuleId, SourceRange};
pub use source_range::SourceRange;
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
// Ideally we wouldn't export these things at all, they should only be used for testing.

View File

@ -0,0 +1,157 @@
use std::{fmt, path::PathBuf};
use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
execution::PreImportedGeometry,
fs::{FileManager, FileSystem},
parsing::ast::types::{ImportPath, Node, Program},
source_range::SourceRange,
};
/// Identifier of a source file. Uses a u32 to keep the size small.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ModuleId(u32);
impl ModuleId {
pub fn from_usize(id: usize) -> Self {
Self(u32::try_from(id).expect("module ID should fit in a u32"))
}
pub fn as_usize(&self) -> usize {
usize::try_from(self.0).expect("module ID should fit in a usize")
}
/// Top-level file is the one being executed.
/// Represented by module ID of 0, i.e. the default value.
pub fn is_top_level(&self) -> bool {
*self == Self::default()
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ModuleLoader {
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<PathBuf>,
}
impl ModuleLoader {
pub(crate) fn cycle_check(&self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> {
if self.import_stack.contains(path.expect_path()) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
self.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
path,
),
source_ranges: vec![source_range],
}));
}
Ok(())
}
pub(crate) fn enter_module(&mut self, path: &ModulePath) {
if let ModulePath::Local(ref path) = path {
self.import_stack.push(path.clone());
}
}
pub(crate) fn leave_module(&mut self, path: &ModulePath) {
if let ModulePath::Local(ref path) = path {
let popped = self.import_stack.pop().unwrap();
assert_eq!(path, &popped);
}
}
}
pub(crate) fn read_std(_mod_name: &str) -> Option<&'static str> {
None
}
/// Info about a module. Right now, this is pretty minimal. We hope to cache
/// modules here in the future.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ModuleInfo {
/// The ID of the module.
pub(crate) id: ModuleId,
/// Absolute path of the module's source file.
pub(crate) path: ModulePath,
pub(crate) repr: ModuleRepr,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ModuleRepr {
Root,
Kcl(Node<Program>),
Foreign(PreImportedGeometry),
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)]
pub enum ModulePath {
Local(PathBuf),
Std(String),
}
impl ModulePath {
pub(crate) fn expect_path(&self) -> &PathBuf {
match self {
ModulePath::Local(p) => p,
_ => unreachable!(),
}
}
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<String, KclError> {
match self {
ModulePath::Local(p) => fs.read_to_string(p, source_range).await,
ModulePath::Std(name) => read_std(name)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Cannot find standard library module to import: std::{name}."),
source_ranges: vec![source_range],
})
})
.map(str::to_owned),
}
}
pub(crate) fn from_import_path(path: &ImportPath, project_directory: &Option<PathBuf>) -> Self {
match path {
ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
let resolved_path = if let Some(project_dir) = project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(path)
};
ModulePath::Local(resolved_path)
}
ImportPath::Std { path } => {
// For now we only support importing from singly-nested modules inside std.
assert_eq!(path.len(), 2);
assert_eq!(&path[0], "std");
ModulePath::Std(path[1].clone())
}
}
}
}
impl fmt::Display for ModulePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ModulePath::Local(path) => path.display().fmt(f),
ModulePath::Std(s) => write!(f, "std::{s}"),
}
}
}

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